craft-native 0.0.160 → 0.0.162
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/dist/android/templates/CraftNative.kt.template +40 -2
- package/dist/android/templates/CraftNativeCapabilities.kt.template +4 -2
- package/dist/android/templates/MainActivityNative.kt.template +41 -7
- package/dist/api/mobile.d.ts +55 -4
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +9 -0
- package/dist/ios/src/index.js +2 -0
- package/dist/ios/templates/CraftApp.swift +24 -0
- package/dist/ios/templates/CraftHealth.swift +112 -0
- package/dist/ios/templates/CraftNativeScreen.swift +105 -16
- package/dist/mobile.js +12 -0
- package/package.json +1 -1
|
@@ -298,6 +298,43 @@ object CraftNative {
|
|
|
298
298
|
margins: FloatArray,
|
|
299
299
|
flags: FloatArray,
|
|
300
300
|
): FloatArray?
|
|
301
|
+
private external fun nativeGridAligned(
|
|
302
|
+
availableWidth: Float,
|
|
303
|
+
availableHeight: Float,
|
|
304
|
+
columns: Int,
|
|
305
|
+
columnGap: Float,
|
|
306
|
+
rowGap: Float,
|
|
307
|
+
alignContent: Int,
|
|
308
|
+
intrinsicWidth: FloatArray,
|
|
309
|
+
intrinsicHeight: FloatArray,
|
|
310
|
+
explicitWidth: FloatArray,
|
|
311
|
+
explicitHeight: FloatArray,
|
|
312
|
+
minWidth: FloatArray,
|
|
313
|
+
maxWidth: FloatArray,
|
|
314
|
+
minHeight: FloatArray,
|
|
315
|
+
maxHeight: FloatArray,
|
|
316
|
+
margins: FloatArray,
|
|
317
|
+
flags: FloatArray,
|
|
318
|
+
): FloatArray?
|
|
319
|
+
private external fun nativeGridAutoRowsAligned(
|
|
320
|
+
availableWidth: Float,
|
|
321
|
+
availableHeight: Float,
|
|
322
|
+
columns: Int,
|
|
323
|
+
columnGap: Float,
|
|
324
|
+
rowGap: Float,
|
|
325
|
+
alignContent: Int,
|
|
326
|
+
autoRowHeight: Float,
|
|
327
|
+
intrinsicWidth: FloatArray,
|
|
328
|
+
intrinsicHeight: FloatArray,
|
|
329
|
+
explicitWidth: FloatArray,
|
|
330
|
+
explicitHeight: FloatArray,
|
|
331
|
+
minWidth: FloatArray,
|
|
332
|
+
maxWidth: FloatArray,
|
|
333
|
+
minHeight: FloatArray,
|
|
334
|
+
maxHeight: FloatArray,
|
|
335
|
+
margins: FloatArray,
|
|
336
|
+
flags: FloatArray,
|
|
337
|
+
): FloatArray?
|
|
301
338
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
302
339
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
303
340
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
@@ -532,13 +569,14 @@ object CraftNative {
|
|
|
532
569
|
margins: FloatArray,
|
|
533
570
|
flags: FloatArray,
|
|
534
571
|
autoRowHeight: Float? = null,
|
|
572
|
+
alignContent: Int = 0,
|
|
535
573
|
): FloatArray? {
|
|
536
574
|
if (!isAvailable) return null
|
|
537
575
|
return try {
|
|
538
576
|
if (autoRowHeight != null) {
|
|
539
|
-
|
|
577
|
+
nativeGridAutoRowsAligned(availableWidth, availableHeight, columns, columnGap, rowGap, alignContent, autoRowHeight, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
|
|
540
578
|
} else {
|
|
541
|
-
|
|
579
|
+
nativeGridAligned(availableWidth, availableHeight, columns, columnGap, rowGap, alignContent, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
|
|
542
580
|
}
|
|
543
581
|
?.takeIf { it.size >= intrinsicWidth.size * 4 && it.all { value -> value.isFinite() } }
|
|
544
582
|
} catch (e: UnsatisfiedLinkError) {
|
|
@@ -303,7 +303,8 @@ internal class CraftNativeCapabilities(
|
|
|
303
303
|
return FileResult.Multipart(body.toByteArray(), boundary)
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult
|
|
306
|
+
private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
307
|
+
return when (method) {
|
|
307
308
|
"write" -> {
|
|
308
309
|
val name = args.optString(0).trim()
|
|
309
310
|
if (name.isBlank()) return failure("INVALID_ARGUMENT", "files.write needs a file name")
|
|
@@ -322,7 +323,8 @@ internal class CraftNativeCapabilities(
|
|
|
322
323
|
success(file.delete())
|
|
323
324
|
}
|
|
324
325
|
"download" -> download(requestToken, args)
|
|
325
|
-
|
|
326
|
+
else -> failure("UNKNOWN_ACTION", "Unsupported Files method")
|
|
327
|
+
}
|
|
326
328
|
}
|
|
327
329
|
|
|
328
330
|
private fun download(requestToken: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
@@ -115,6 +115,7 @@ private data class CraftNativeLength(
|
|
|
115
115
|
private data class CraftNativeLayoutStyle(
|
|
116
116
|
val width: CraftNativeLength? = null,
|
|
117
117
|
val height: CraftNativeLength? = null,
|
|
118
|
+
val aspectRatio: Double? = null,
|
|
118
119
|
val minWidth: CraftNativeLength? = null,
|
|
119
120
|
val maxWidth: CraftNativeLength? = null,
|
|
120
121
|
val minHeight: CraftNativeLength? = null,
|
|
@@ -143,6 +144,7 @@ private data class CraftNativeLayoutStyle(
|
|
|
143
144
|
val flex = decimal("flex")
|
|
144
145
|
return CraftNativeLayoutStyle(
|
|
145
146
|
width = length("width"), height = length("height"),
|
|
147
|
+
aspectRatio = decimal("aspectRatio")?.takeIf { it.isFinite() && it > 0.0 },
|
|
146
148
|
minWidth = length("minWidth"), maxWidth = length("maxWidth"),
|
|
147
149
|
minHeight = length("minHeight"), maxHeight = length("maxHeight"),
|
|
148
150
|
position = raw.opt("position")
|
|
@@ -583,7 +585,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
583
585
|
// The STX style is authoritative. A reused View may still carry a
|
|
584
586
|
// previous fixed LayoutParams width after a reactive width is removed;
|
|
585
587
|
// using that value here prevents intrinsic sizing from being restored.
|
|
586
|
-
val
|
|
588
|
+
val authored = style.width?.resolve(base)
|
|
589
|
+
val explicit = authored ?: style.aspectRatio?.let { ratio -> style.height?.resolve(base)?.times(ratio)?.roundToInt() } ?: flexBasis
|
|
587
590
|
if (explicit != null) {
|
|
588
591
|
child.measure(
|
|
589
592
|
View.MeasureSpec.makeMeasureSpec(explicit, View.MeasureSpec.EXACTLY),
|
|
@@ -613,7 +616,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
613
616
|
val flexBasis = if (orientation == VERTICAL) style.flexBasis?.resolve(base) else null
|
|
614
617
|
// Match width measurement: do not let stale native LayoutParams keep
|
|
615
618
|
// a removed reactive height fixed.
|
|
616
|
-
val
|
|
619
|
+
val authored = style.height?.resolve(base)
|
|
620
|
+
val explicit = authored ?: style.aspectRatio?.let { ratio -> style.width?.resolve(base)?.div(ratio)?.roundToInt() } ?: flexBasis
|
|
617
621
|
val target = explicit ?: base
|
|
618
622
|
val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
|
|
619
623
|
else View.MeasureSpec.makeMeasureSpec(base, View.MeasureSpec.AT_MOST)
|
|
@@ -649,6 +653,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
649
653
|
"space-between" -> 3
|
|
650
654
|
"space-around" -> 4
|
|
651
655
|
"stretch" -> 5
|
|
656
|
+
"space-evenly" -> 6
|
|
652
657
|
else -> 0
|
|
653
658
|
}
|
|
654
659
|
|
|
@@ -772,6 +777,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
772
777
|
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
773
778
|
}.toFloatArray(),
|
|
774
779
|
gridAutoRows?.resolve(contentHeight)?.toFloat(),
|
|
780
|
+
alignContentCode(),
|
|
775
781
|
)
|
|
776
782
|
if (sharedFrames != null) {
|
|
777
783
|
for ((index, entry) in flow.withIndex()) {
|
|
@@ -783,9 +789,30 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
783
789
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
784
790
|
}
|
|
785
791
|
} else {
|
|
786
|
-
|
|
792
|
+
val totalRows = layout.rowHeights.sum()
|
|
793
|
+
val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
|
|
794
|
+
val freeRows = max(0, contentHeight - totalRows - rowGaps)
|
|
795
|
+
val rowCount = max(1, layout.rowHeights.size)
|
|
796
|
+
var rowLeading = 0
|
|
797
|
+
var rowBetween = rowGap
|
|
798
|
+
var rowExtra = 0
|
|
799
|
+
when (alignContentCode()) {
|
|
800
|
+
1 -> rowLeading = freeRows / 2
|
|
801
|
+
2 -> rowLeading = freeRows
|
|
802
|
+
3 -> if (rowCount > 1) rowBetween += freeRows / (rowCount - 1)
|
|
803
|
+
4 -> {
|
|
804
|
+
rowLeading = freeRows / (rowCount * 2)
|
|
805
|
+
rowBetween += freeRows / rowCount
|
|
806
|
+
}
|
|
807
|
+
6 -> {
|
|
808
|
+
rowLeading = freeRows / (rowCount + 1)
|
|
809
|
+
rowBetween += freeRows / (rowCount + 1)
|
|
810
|
+
}
|
|
811
|
+
5 -> rowExtra = freeRows / rowCount
|
|
812
|
+
}
|
|
813
|
+
var rowTop = paddingTop + rowLeading
|
|
787
814
|
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
788
|
-
val rowHeight = layout.rowHeights[rowIndex]
|
|
815
|
+
val rowHeight = layout.rowHeights[rowIndex] + rowExtra
|
|
789
816
|
for ((column, entry) in row.withIndex()) {
|
|
790
817
|
val align = entry.style.alignSelf ?: alignItems
|
|
791
818
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
@@ -809,7 +836,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
809
836
|
}
|
|
810
837
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
811
838
|
}
|
|
812
|
-
rowTop += rowHeight +
|
|
839
|
+
rowTop += rowHeight + rowBetween
|
|
813
840
|
}
|
|
814
841
|
}
|
|
815
842
|
for (entry in layout.absolute) {
|
|
@@ -849,6 +876,10 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
849
876
|
crossBetween = crossFree / lines.size + crossGap
|
|
850
877
|
crossLeading = crossFree / (lines.size * 2)
|
|
851
878
|
}
|
|
879
|
+
"space-evenly" -> {
|
|
880
|
+
crossBetween = crossFree / (lines.size + 1) + crossGap
|
|
881
|
+
crossLeading = crossFree / (lines.size + 1)
|
|
882
|
+
}
|
|
852
883
|
"stretch" -> {
|
|
853
884
|
val extra = crossFree / lines.size
|
|
854
885
|
lines.forEach { it.cross += extra }
|
|
@@ -2803,7 +2834,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2803
2834
|
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
2804
2835
|
drawing = node
|
|
2805
2836
|
}
|
|
2806
|
-
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2837
|
+
"ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2807
2838
|
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
2808
2839
|
setHorizontal(horizontal)
|
|
2809
2840
|
setNativeNestedScrollingEnabled(true)
|
|
@@ -3064,6 +3095,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3064
3095
|
"video" -> "Video"
|
|
3065
3096
|
"icon" -> "Icon"
|
|
3066
3097
|
"scrollview" -> "ScrollView"
|
|
3098
|
+
"sectionlist" -> "SectionList"
|
|
3067
3099
|
"flatlist" -> "FlatList"
|
|
3068
3100
|
"svg" -> "Svg"
|
|
3069
3101
|
"touchableopacity" -> "TouchableOpacity"
|
|
@@ -3477,7 +3509,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3477
3509
|
else -> null
|
|
3478
3510
|
}
|
|
3479
3511
|
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
3480
|
-
val role = props.optString("accessibilityRole").ifBlank {
|
|
3512
|
+
val role = props.optString("accessibilityRole").ifBlank {
|
|
3513
|
+
if (type == "Icon") "image" else type.lowercase()
|
|
3514
|
+
}
|
|
3481
3515
|
val state = props.optJSONObject("accessibilityState")
|
|
3482
3516
|
val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
|
|
3483
3517
|
if (type !in setOf("Button", "Link", "Checkbox", "TextInput", "Picker", "DatePicker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
|
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1240,9 +1240,11 @@ export declare const pushNotifications: {
|
|
|
1240
1240
|
};
|
|
1241
1241
|
export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep'
|
|
1242
1242
|
/** Food logged by any app, per day: kcal eaten, then grams of protein, carbohydrate and fat. Read only. */
|
|
1243
|
-
| 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat'
|
|
1243
|
+
| 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat'
|
|
1244
|
+
/** Mindful minutes (meditation, breathing): read with getMindfulSessions, written with saveMindfulSession. */
|
|
1245
|
+
| 'mindful';
|
|
1244
1246
|
/** The types with one value per day. `sleep` is hours asleep, counted on the day you woke. */
|
|
1245
|
-
export type HealthDailyType = Exclude<HealthDataType, 'workouts'>;
|
|
1247
|
+
export type HealthDailyType = Exclude<HealthDataType, 'workouts' | 'mindful'>;
|
|
1246
1248
|
export interface HealthAuthorizationOptions {
|
|
1247
1249
|
/** `false` asks to read only. Default: also allowed to save workouts. */
|
|
1248
1250
|
write?: boolean;
|
|
@@ -1350,8 +1352,49 @@ export interface HealthSleepInterval {
|
|
|
1350
1352
|
sourceName: string;
|
|
1351
1353
|
deviceName?: string;
|
|
1352
1354
|
}
|
|
1353
|
-
/**
|
|
1354
|
-
export
|
|
1355
|
+
/** A meditation or breathing session to record as mindful minutes. */
|
|
1356
|
+
export interface HealthMindfulSession {
|
|
1357
|
+
/**
|
|
1358
|
+
* The app's own id for the session (up to 128 characters), kept as the
|
|
1359
|
+
* sample's external UUID. Saving the same id again keeps the first sample.
|
|
1360
|
+
*/
|
|
1361
|
+
id: string;
|
|
1362
|
+
/** Epoch milliseconds. */
|
|
1363
|
+
startDate: number;
|
|
1364
|
+
/** Epoch milliseconds, after `startDate` and at most a day later. */
|
|
1365
|
+
endDate: number;
|
|
1366
|
+
}
|
|
1367
|
+
export interface HealthMindfulSessionResult {
|
|
1368
|
+
/** HealthKit's UUID for the sample. */
|
|
1369
|
+
id: string;
|
|
1370
|
+
/** The id it was saved with. */
|
|
1371
|
+
externalId: string;
|
|
1372
|
+
/** True when this id was saved before and that sample was kept. */
|
|
1373
|
+
existing: boolean;
|
|
1374
|
+
}
|
|
1375
|
+
export interface HealthMindfulSessionQuery extends HealthSampleQuery {
|
|
1376
|
+
/** At most this many, oldest first. Defaults to 1000, capped at 5000. Start defaults to 30 days ago. */
|
|
1377
|
+
limit?: number;
|
|
1378
|
+
}
|
|
1379
|
+
/** A mindful session in Apple Health, from any app that writes them. */
|
|
1380
|
+
export interface HealthMindfulSample {
|
|
1381
|
+
/** HealthKit's UUID. */
|
|
1382
|
+
id: string;
|
|
1383
|
+
/** Epoch milliseconds. */
|
|
1384
|
+
startDate: number;
|
|
1385
|
+
/** Epoch milliseconds. */
|
|
1386
|
+
endDate: number;
|
|
1387
|
+
durationSeconds: number;
|
|
1388
|
+
sourceName: string;
|
|
1389
|
+
sourceBundleId: string;
|
|
1390
|
+
/** True when this app wrote it. */
|
|
1391
|
+
ownApp: boolean;
|
|
1392
|
+
/** The id the writing app saved it with, when it gave one. */
|
|
1393
|
+
externalId?: string;
|
|
1394
|
+
deviceName?: string;
|
|
1395
|
+
}
|
|
1396
|
+
/** The health reads and writes a host may or may not have, depending on the app build it runs in. */
|
|
1397
|
+
export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveMindfulSession' | 'getMindfulSessions';
|
|
1355
1398
|
export declare const health: {
|
|
1356
1399
|
/**
|
|
1357
1400
|
* Whether this app build answers `method`. An app installed before a read
|
|
@@ -1374,6 +1417,14 @@ export declare const health: {
|
|
|
1374
1417
|
getHeartRateSamples(options?: HealthHeartRateQuery): Promise<HealthHeartRateSample[]>;
|
|
1375
1418
|
/** Sleep between two instants, oldest first: time in bed and each asleep or awake stage. iOS only for now. */
|
|
1376
1419
|
getSleepIntervals(options?: HealthSampleQuery): Promise<HealthSleepInterval[]>;
|
|
1420
|
+
/**
|
|
1421
|
+
* Record a meditation or breathing session as mindful minutes. Needs the
|
|
1422
|
+
* `mindful` grant with writing allowed. Saving the same `id` again keeps
|
|
1423
|
+
* the first sample. iOS only for now.
|
|
1424
|
+
*/
|
|
1425
|
+
saveMindfulSession(session: HealthMindfulSession): Promise<HealthMindfulSessionResult>;
|
|
1426
|
+
/** Mindful sessions between two instants, oldest first, each marked `ownApp` when this app wrote it. iOS only for now. */
|
|
1427
|
+
getMindfulSessions(options?: HealthMindfulSessionQuery): Promise<HealthMindfulSample[]>;
|
|
1377
1428
|
};
|
|
1378
1429
|
export interface LiveActivityState {
|
|
1379
1430
|
status?: string;
|
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.162";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -45,7 +45,7 @@ export interface FlexStyle {
|
|
|
45
45
|
/** Align self (override parent alignItems) */
|
|
46
46
|
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
47
47
|
/** Align content (multi-line) */
|
|
48
|
-
alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around';
|
|
48
|
+
alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
|
|
49
49
|
/** Gap between items */
|
|
50
50
|
gap?: number;
|
|
51
51
|
/** Row gap */
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -79,6 +79,15 @@ export interface CraftConfig {
|
|
|
79
79
|
* time the page scans or connects.
|
|
80
80
|
*/
|
|
81
81
|
enableHeartRate?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Keep playing sound with the screen locked or the app in the background:
|
|
84
|
+
* a sleep soundscape, a meditation timer's ambience. Adds the `audio`
|
|
85
|
+
* background mode. iOS keeps the app running only while it plays, and App
|
|
86
|
+
* Review expects audible content, so turn it on only for an app that plays
|
|
87
|
+
* some (Web Audio on native screens and in the web view, which use the
|
|
88
|
+
* playback session).
|
|
89
|
+
*/
|
|
90
|
+
enableBackgroundAudio?: boolean;
|
|
82
91
|
/**
|
|
83
92
|
* Purpose strings in the app's own words, by Info.plist key
|
|
84
93
|
* (`{ NSAppleMusicUsageDescription: 'Shows the song playing during a workout.' }`).
|
package/dist/ios/src/index.js
CHANGED
|
@@ -490,6 +490,8 @@ function renderBackgroundModes(config) {
|
|
|
490
490
|
modes.add("remote-notification");
|
|
491
491
|
if (config.enableHeartRate)
|
|
492
492
|
modes.add("bluetooth-central");
|
|
493
|
+
if (config.enableBackgroundAudio)
|
|
494
|
+
modes.add("audio");
|
|
493
495
|
if (!modes.size)
|
|
494
496
|
return "";
|
|
495
497
|
return ` <key>UIBackgroundModes</key>
|
|
@@ -2548,6 +2548,22 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2548
2548
|
} else {
|
|
2549
2549
|
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2550
2550
|
}
|
|
2551
|
+
case "saveHealthMindfulSession":
|
|
2552
|
+
if config.enableHealthKit {
|
|
2553
|
+
CraftHealth.shared.saveMindfulSession(body: body) { [weak self] in self?.answer(callbackId, $0) }
|
|
2554
|
+
} else {
|
|
2555
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2556
|
+
}
|
|
2557
|
+
case "getHealthMindfulSessions":
|
|
2558
|
+
if config.enableHealthKit {
|
|
2559
|
+
CraftHealth.shared.getMindfulSessions(
|
|
2560
|
+
startDate: body["startDate"] as? Double,
|
|
2561
|
+
endDate: body["endDate"] as? Double,
|
|
2562
|
+
limit: body["limit"] as? Int
|
|
2563
|
+
) { [weak self] in self?.answer(callbackId, $0) }
|
|
2564
|
+
} else {
|
|
2565
|
+
rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
|
|
2566
|
+
}
|
|
2551
2567
|
|
|
2552
2568
|
// MARK: - Live Activities
|
|
2553
2569
|
case "startLiveActivity":
|
|
@@ -5056,6 +5072,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5056
5072
|
getSleepIntervals: function(options) {
|
|
5057
5073
|
options = options || {};
|
|
5058
5074
|
return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
|
|
5075
|
+
},
|
|
5076
|
+
saveMindfulSession: function(session) {
|
|
5077
|
+
session = session || {};
|
|
5078
|
+
return craft._invoke('saveHealthMindfulSession', {id: session.id, startDate: session.startDate, endDate: session.endDate});
|
|
5079
|
+
},
|
|
5080
|
+
getMindfulSessions: function(options) {
|
|
5081
|
+
options = options || {};
|
|
5082
|
+
return craft._invoke('getHealthMindfulSessions', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
|
|
5059
5083
|
}
|
|
5060
5084
|
};
|
|
5061
5085
|
// The Music app's song and transport. Nothing is read until
|
|
@@ -43,6 +43,10 @@ final class CraftHealth {
|
|
|
43
43
|
getHeartRateSamples(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
|
|
44
44
|
case "getSleepIntervals":
|
|
45
45
|
getSleepIntervals(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, completion: completion)
|
|
46
|
+
case "saveMindfulSession":
|
|
47
|
+
saveMindfulSession(body: options(0), completion: completion)
|
|
48
|
+
case "getMindfulSessions":
|
|
49
|
+
getMindfulSessions(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
|
|
46
50
|
default:
|
|
47
51
|
completion(.failure(CraftHealthError(message: "Unsupported Health method", code: "UNKNOWN_ACTION")))
|
|
48
52
|
}
|
|
@@ -106,6 +110,14 @@ final class CraftHealth {
|
|
|
106
110
|
if let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) {
|
|
107
111
|
readTypes.insert(sleepType)
|
|
108
112
|
}
|
|
113
|
+
case "mindful":
|
|
114
|
+
// Mindful minutes: a meditation or breathing session. Read
|
|
115
|
+
// to show them, and (unless read only) written by an app
|
|
116
|
+
// that times them.
|
|
117
|
+
if let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) {
|
|
118
|
+
readTypes.insert(mindfulType)
|
|
119
|
+
shareTypes.insert(mindfulType)
|
|
120
|
+
}
|
|
109
121
|
case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
|
|
110
122
|
// What a food log (MyFitnessPal, Lose It!, Cronometer...)
|
|
111
123
|
// writes to Apple Health: read only, never written back.
|
|
@@ -559,4 +571,104 @@ final class CraftHealth {
|
|
|
559
571
|
healthStore.execute(query)
|
|
560
572
|
}
|
|
561
573
|
|
|
574
|
+
/// Saves a mindful session (Mindful Minutes in the Health app): a
|
|
575
|
+
/// meditation or breathing session that ran from `startDate` to
|
|
576
|
+
/// `endDate`, epoch milliseconds. `id` is the app's own id for it, kept
|
|
577
|
+
/// as the sample's external UUID, so saving the same session twice (a
|
|
578
|
+
/// retry, a second device) keeps the first sample instead of adding
|
|
579
|
+
/// another: the answer then says `existing: true`.
|
|
580
|
+
func saveMindfulSession(body: [String: Any], completion: @escaping Completion) {
|
|
581
|
+
guard let healthStore = healthStore else {
|
|
582
|
+
completion(.failure(CraftHealthError(message: "HealthKit not available")))
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
586
|
+
completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
guard let id = body["id"] as? String, !id.isEmpty, id.count <= 128,
|
|
590
|
+
let startValue = body["startDate"] as? Double,
|
|
591
|
+
let endValue = body["endDate"] as? Double,
|
|
592
|
+
endValue > startValue,
|
|
593
|
+
endValue - startValue <= 24 * 3600 * 1000 else {
|
|
594
|
+
completion(.failure(CraftHealthError(message: "A valid id, startDate and endDate (at most a day apart) are required", code: "INVALID_ARGUMENT")))
|
|
595
|
+
return
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let start = Date(timeIntervalSince1970: startValue / 1000)
|
|
599
|
+
let end = Date(timeIntervalSince1970: endValue / 1000)
|
|
600
|
+
// Only this app's own samples can be the same session.
|
|
601
|
+
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
|
|
602
|
+
HKQuery.predicateForObjects(withMetadataKey: HKMetadataKeyExternalUUID, allowedValues: [id]),
|
|
603
|
+
HKQuery.predicateForObjects(from: HKSource.default()),
|
|
604
|
+
])
|
|
605
|
+
let lookup = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: 1, sortDescriptors: nil) { _, samples, _ in
|
|
606
|
+
if let kept = samples?.first {
|
|
607
|
+
completion(.success(["id": kept.uuid.uuidString, "externalId": id, "existing": true]))
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
let sample = HKCategorySample(
|
|
611
|
+
type: mindfulType,
|
|
612
|
+
value: HKCategoryValue.notApplicable.rawValue,
|
|
613
|
+
start: start,
|
|
614
|
+
end: end,
|
|
615
|
+
metadata: [HKMetadataKeyExternalUUID: id]
|
|
616
|
+
)
|
|
617
|
+
healthStore.save(sample) { success, error in
|
|
618
|
+
if success {
|
|
619
|
+
completion(.success(["id": sample.uuid.uuidString, "externalId": id, "existing": false]))
|
|
620
|
+
} else {
|
|
621
|
+
completion(.failure(CraftHealthError(message: error?.localizedDescription ?? "Mindful session could not be saved")))
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
healthStore.execute(lookup)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/// Mindful sessions between two instants, oldest first, from every app
|
|
629
|
+
/// that writes them (the Mindfulness app, Calm, this one). Each says
|
|
630
|
+
/// whether this app wrote it (`ownApp`) and carries the external id it
|
|
631
|
+
/// was saved with, so a page can tell its own sessions from the rest.
|
|
632
|
+
func getMindfulSessions(startDate: Double?, endDate: Double?, limit: Int?, completion: @escaping Completion) {
|
|
633
|
+
guard let healthStore = healthStore else {
|
|
634
|
+
completion(.failure(CraftHealthError(message: "HealthKit not available")))
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
|
|
638
|
+
completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
|
|
643
|
+
let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -30, to: end)!
|
|
644
|
+
let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
|
|
645
|
+
let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
646
|
+
let cap = max(1, min(limit ?? 1000, 5000))
|
|
647
|
+
let ownBundle = Bundle.main.bundleIdentifier
|
|
648
|
+
|
|
649
|
+
let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { _, samples, error in
|
|
650
|
+
if let error = error, (error as? HKError)?.code != .errorNoData {
|
|
651
|
+
completion(.failure(CraftHealthError(message: error.localizedDescription)))
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
let rows: [[String: Any]] = (samples ?? []).map { sample in
|
|
655
|
+
let source = sample.sourceRevision.source
|
|
656
|
+
var item: [String: Any] = [
|
|
657
|
+
"id": sample.uuid.uuidString,
|
|
658
|
+
"startDate": sample.startDate.timeIntervalSince1970 * 1000,
|
|
659
|
+
"endDate": sample.endDate.timeIntervalSince1970 * 1000,
|
|
660
|
+
"durationSeconds": sample.endDate.timeIntervalSince(sample.startDate),
|
|
661
|
+
"sourceName": source.name,
|
|
662
|
+
"sourceBundleId": source.bundleIdentifier,
|
|
663
|
+
"ownApp": ownBundle != nil && source.bundleIdentifier == ownBundle,
|
|
664
|
+
]
|
|
665
|
+
if let external = sample.metadata?[HKMetadataKeyExternalUUID] as? String { item["externalId"] = external }
|
|
666
|
+
if let device = sample.device?.name { item["deviceName"] = device }
|
|
667
|
+
return item
|
|
668
|
+
}
|
|
669
|
+
completion(.success(rows))
|
|
670
|
+
}
|
|
671
|
+
healthStore.execute(query)
|
|
672
|
+
}
|
|
673
|
+
|
|
562
674
|
}
|
|
@@ -185,6 +185,12 @@ private enum CraftNativeGridBridge {
|
|
|
185
185
|
private typealias AutoRowsFunction = @convention(c) (
|
|
186
186
|
Float, Float, UInt32, Float, Float, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer
|
|
187
187
|
) -> Int32
|
|
188
|
+
private typealias AlignedFunction = @convention(c) (
|
|
189
|
+
Float, Float, UInt32, Float, Float, UInt32, UnsafeRawPointer, Int, UnsafeMutableRawPointer
|
|
190
|
+
) -> Int32
|
|
191
|
+
private typealias AutoRowsAlignedFunction = @convention(c) (
|
|
192
|
+
Float, Float, UInt32, Float, Float, UInt32, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer
|
|
193
|
+
) -> Int32
|
|
188
194
|
|
|
189
195
|
private static let function: Function? = {
|
|
190
196
|
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid") else { return nil }
|
|
@@ -194,6 +200,14 @@ private enum CraftNativeGridBridge {
|
|
|
194
200
|
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_auto_rows") else { return nil }
|
|
195
201
|
return unsafeBitCast(symbol, to: AutoRowsFunction.self)
|
|
196
202
|
}()
|
|
203
|
+
private static let alignedFunction: AlignedFunction? = {
|
|
204
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_aligned") else { return nil }
|
|
205
|
+
return unsafeBitCast(symbol, to: AlignedFunction.self)
|
|
206
|
+
}()
|
|
207
|
+
private static let autoRowsAlignedFunction: AutoRowsAlignedFunction? = {
|
|
208
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_auto_rows_aligned") else { return nil }
|
|
209
|
+
return unsafeBitCast(symbol, to: AutoRowsAlignedFunction.self)
|
|
210
|
+
}()
|
|
197
211
|
|
|
198
212
|
static func frames(
|
|
199
213
|
availableWidth: CGFloat,
|
|
@@ -202,7 +216,8 @@ private enum CraftNativeGridBridge {
|
|
|
202
216
|
columnGap: CGFloat,
|
|
203
217
|
rowGap: CGFloat,
|
|
204
218
|
items: [(intrinsicWidth: CGFloat, intrinsicHeight: CGFloat, explicitWidth: CGFloat?, explicitHeight: CGFloat?, minWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, maxHeight: CGFloat?, marginLeft: CGFloat, marginRight: CGFloat, marginTop: CGFloat, marginBottom: CGFloat, stretch: Bool, alignment: UInt32)],
|
|
205
|
-
autoRowHeight: CGFloat? = nil
|
|
219
|
+
autoRowHeight: CGFloat? = nil,
|
|
220
|
+
alignContent: UInt32 = 0
|
|
206
221
|
) -> [(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat)]? {
|
|
207
222
|
guard !items.isEmpty else { return nil }
|
|
208
223
|
if autoRowHeight != nil, autoRowsFunction == nil { return nil }
|
|
@@ -222,8 +237,14 @@ private enum CraftNativeGridBridge {
|
|
|
222
237
|
let status = input.withUnsafeBufferPointer { inputBuffer in
|
|
223
238
|
output.withUnsafeMutableBufferPointer { outputBuffer in
|
|
224
239
|
if let autoRowHeight, let autoRowsFunction {
|
|
240
|
+
if alignContent != 0, let autoRowsAlignedFunction {
|
|
241
|
+
return autoRowsAlignedFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), alignContent, Float(autoRowHeight), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
242
|
+
}
|
|
225
243
|
return autoRowsFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), Float(autoRowHeight), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
226
244
|
}
|
|
245
|
+
if alignContent != 0, let alignedFunction {
|
|
246
|
+
return alignedFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), alignContent, UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
247
|
+
}
|
|
227
248
|
guard let function else { return Int32(-1) }
|
|
228
249
|
return function(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
|
|
229
250
|
}
|
|
@@ -235,6 +256,18 @@ private enum CraftNativeGridBridge {
|
|
|
235
256
|
}
|
|
236
257
|
}
|
|
237
258
|
|
|
259
|
+
private func craftNativeAlignContentCode(_ value: String) -> UInt32 {
|
|
260
|
+
switch value {
|
|
261
|
+
case "center": return 1
|
|
262
|
+
case "flex-end": return 2
|
|
263
|
+
case "space-between": return 3
|
|
264
|
+
case "space-around": return 4
|
|
265
|
+
case "stretch": return 5
|
|
266
|
+
case "space-evenly": return 6
|
|
267
|
+
default: return 0
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
238
271
|
private final class CraftNativeVideoView: UIView {
|
|
239
272
|
private let playerController = AVPlayerViewController()
|
|
240
273
|
private var player: AVPlayer?
|
|
@@ -436,6 +469,7 @@ private enum CraftNativeLength: Equatable {
|
|
|
436
469
|
private struct CraftNativeLayoutStyle {
|
|
437
470
|
let width: CraftNativeLength?
|
|
438
471
|
let height: CraftNativeLength?
|
|
472
|
+
let aspectRatio: CGFloat?
|
|
439
473
|
let minWidth: CraftNativeLength?
|
|
440
474
|
let maxWidth: CraftNativeLength?
|
|
441
475
|
let minHeight: CraftNativeLength?
|
|
@@ -465,6 +499,8 @@ private struct CraftNativeLayoutStyle {
|
|
|
465
499
|
func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
466
500
|
width = CraftNativeLength(raw["width"])
|
|
467
501
|
height = CraftNativeLength(raw["height"])
|
|
502
|
+
let ratio = number(raw["aspectRatio"])
|
|
503
|
+
aspectRatio = ratio.flatMap { $0.isFinite && $0 > 0 ? $0 : nil }
|
|
468
504
|
minWidth = CraftNativeLength(raw["minWidth"])
|
|
469
505
|
maxWidth = CraftNativeLength(raw["maxWidth"])
|
|
470
506
|
minHeight = CraftNativeLength(raw["minHeight"])
|
|
@@ -870,12 +906,13 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
870
906
|
let content = CGRect(x: padding.left, y: padding.top, width: max(0, size.width - paddingWidth), height: max(0, size.height - paddingHeight))
|
|
871
907
|
for child in children where child.craftLayoutStyle.position == "absolute" {
|
|
872
908
|
let style = child.craftLayoutStyle
|
|
873
|
-
|
|
909
|
+
var fixedWidth = style.left != nil && style.right != nil
|
|
874
910
|
? max(0, content.width - (style.left ?? 0) - (style.right ?? 0))
|
|
875
911
|
: style.width?.resolve(content.width)
|
|
876
|
-
|
|
912
|
+
var fixedHeight = style.top != nil && style.bottom != nil
|
|
877
913
|
? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0))
|
|
878
914
|
: style.height?.resolve(content.height)
|
|
915
|
+
(fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
|
|
879
916
|
let measured = CraftNativeFlexLayout.measure(child, width: fixedWidth, height: fixedHeight, maxWidth: content.width, maxHeight: content.height)
|
|
880
917
|
let x = style.left ?? (style.right.map { content.width - $0 - measured.width } ?? 0)
|
|
881
918
|
let y = style.top ?? (style.bottom.map { content.height - $0 - measured.height } ?? 0)
|
|
@@ -885,6 +922,21 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
885
922
|
return (size, frames)
|
|
886
923
|
}
|
|
887
924
|
|
|
925
|
+
/// Resolves the missing side of an aspect-ratio box after its parent has
|
|
926
|
+
/// supplied any definite width or height. Keeping this at the host edge
|
|
927
|
+
/// lets grid and absolute children follow the same rule as flex items
|
|
928
|
+
/// without adding platform-specific measurement behavior to native views.
|
|
929
|
+
private func aspectDimensions(
|
|
930
|
+
_ style: CraftNativeLayoutStyle,
|
|
931
|
+
width: CGFloat?,
|
|
932
|
+
height: CGFloat?
|
|
933
|
+
) -> (width: CGFloat?, height: CGFloat?) {
|
|
934
|
+
guard let ratio = style.aspectRatio else { return (width, height) }
|
|
935
|
+
if let width, height == nil { return (width, width / ratio) }
|
|
936
|
+
if let height, width == nil { return (height * ratio, height) }
|
|
937
|
+
return (width, height)
|
|
938
|
+
}
|
|
939
|
+
|
|
888
940
|
private func layoutFlex(
|
|
889
941
|
_ children: [UIView],
|
|
890
942
|
horizontal: Bool,
|
|
@@ -964,8 +1016,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
964
1016
|
let crossLeading = horizontal ? style.marginTop : style.marginLeft
|
|
965
1017
|
let crossTrailing = horizontal ? style.marginBottom : style.marginRight
|
|
966
1018
|
let align = style.alignSelf ?? alignItems
|
|
967
|
-
let
|
|
968
|
-
let
|
|
1019
|
+
let authoredMain = (horizontal ? style.width : style.height)?.resolve(definiteMain)
|
|
1020
|
+
let authoredCross = (horizontal ? style.height : style.width)?.resolve(definiteCross)
|
|
1021
|
+
let explicitMain = authoredMain ?? style.aspectRatio.flatMap { ratio in
|
|
1022
|
+
authoredCross.map { horizontal ? $0 * ratio : $0 / ratio }
|
|
1023
|
+
}
|
|
1024
|
+
let explicitCross = authoredCross ?? style.aspectRatio.flatMap { ratio in
|
|
1025
|
+
authoredMain.map { horizontal ? $0 / ratio : $0 * ratio }
|
|
1026
|
+
}
|
|
969
1027
|
let minMain = (horizontal ? style.minWidth : style.minHeight)?.resolve(definiteMain)
|
|
970
1028
|
let maxMain = (horizontal ? style.maxWidth : style.maxHeight)?.resolve(definiteMain)
|
|
971
1029
|
let minCross = (horizontal ? style.minHeight : style.minWidth)?.resolve(definiteCross)
|
|
@@ -1146,6 +1204,9 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1146
1204
|
case "space-around":
|
|
1147
1205
|
crossBetween = free / CGFloat(lineCrosses.count) + crossGap
|
|
1148
1206
|
crossLeading = free / CGFloat(lineCrosses.count * 2)
|
|
1207
|
+
case "space-evenly":
|
|
1208
|
+
crossBetween = free / CGFloat(lineCrosses.count + 1) + crossGap
|
|
1209
|
+
crossLeading = free / CGFloat(lineCrosses.count + 1)
|
|
1149
1210
|
case "stretch":
|
|
1150
1211
|
let extra = free / CGFloat(lineCrosses.count)
|
|
1151
1212
|
lineCrosses = lineCrosses.map { $0 + extra }
|
|
@@ -1242,11 +1303,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1242
1303
|
let style = child.craftLayoutStyle
|
|
1243
1304
|
let available = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1244
1305
|
let align = style.alignSelf ?? alignItems
|
|
1245
|
-
|
|
1306
|
+
var fixedWidth = style.width?.resolve(cellWidth)
|
|
1307
|
+
var fixedHeight = style.height?.resolve(nil)
|
|
1308
|
+
if fixedWidth == nil && fixedHeight == nil && align == "stretch" { fixedWidth = available }
|
|
1309
|
+
(fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
|
|
1246
1310
|
let size = CraftNativeFlexLayout.measure(
|
|
1247
1311
|
child,
|
|
1248
1312
|
width: fixedWidth.map { min($0, available) },
|
|
1249
|
-
height:
|
|
1313
|
+
height: fixedHeight,
|
|
1250
1314
|
maxWidth: available,
|
|
1251
1315
|
maxHeight: CraftNativeFlexLayout.unbounded
|
|
1252
1316
|
)
|
|
@@ -1270,16 +1334,23 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1270
1334
|
let align = style.alignSelf ?? alignItems
|
|
1271
1335
|
let size = sizes[index]
|
|
1272
1336
|
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1337
|
+
var explicitWidth = style.width?.resolve(cellWidth)
|
|
1338
|
+
var explicitHeight = style.height?.resolve(nil)
|
|
1339
|
+
if explicitWidth == nil && explicitHeight == nil && align == "stretch" {
|
|
1340
|
+
explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1341
|
+
}
|
|
1342
|
+
(explicitWidth, explicitHeight) = aspectDimensions(style, width: explicitWidth, height: explicitHeight)
|
|
1273
1343
|
return (
|
|
1274
1344
|
size.width, size.height,
|
|
1275
|
-
|
|
1345
|
+
explicitWidth, explicitHeight,
|
|
1276
1346
|
style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
|
|
1277
1347
|
style.minHeight?.resolve(nil), style.maxHeight?.resolve(nil),
|
|
1278
1348
|
style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
|
|
1279
1349
|
align == "stretch", alignment
|
|
1280
1350
|
)
|
|
1281
1351
|
},
|
|
1282
|
-
autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 }
|
|
1352
|
+
autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 },
|
|
1353
|
+
alignContent: craftNativeAlignContentCode(alignContent)
|
|
1283
1354
|
)
|
|
1284
1355
|
if place {
|
|
1285
1356
|
if let sharedFrames {
|
|
@@ -1289,24 +1360,41 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1289
1360
|
}
|
|
1290
1361
|
return CGSize(width: width, height: innerHeight ?? height)
|
|
1291
1362
|
}
|
|
1292
|
-
|
|
1363
|
+
let gridHeight = innerHeight ?? height
|
|
1364
|
+
let gridGaps = CGFloat(max(0, rowHeights.count - 1)) * verticalGap
|
|
1365
|
+
let gridFree = max(0, gridHeight - rowHeights.reduce(0, +) - gridGaps)
|
|
1366
|
+
var rowLeading: CGFloat = 0
|
|
1367
|
+
var rowBetween = verticalGap
|
|
1368
|
+
var rowExtra: CGFloat = 0
|
|
1369
|
+
switch alignContent {
|
|
1370
|
+
case "center": rowLeading = gridFree / 2
|
|
1371
|
+
case "flex-end": rowLeading = gridFree
|
|
1372
|
+
case "space-between":
|
|
1373
|
+
if rowHeights.count > 1 { rowBetween += gridFree / CGFloat(rowHeights.count - 1) }
|
|
1374
|
+
case "space-around":
|
|
1375
|
+
rowLeading = gridFree / CGFloat(rowHeights.count * 2)
|
|
1376
|
+
rowBetween += gridFree / CGFloat(rowHeights.count)
|
|
1377
|
+
case "stretch": rowExtra = gridFree / CGFloat(max(1, rowHeights.count))
|
|
1378
|
+
default: break
|
|
1379
|
+
}
|
|
1380
|
+
var rowTop = padding.top + rowLeading
|
|
1293
1381
|
for (index, child) in children.enumerated() {
|
|
1294
1382
|
let style = child.craftLayoutStyle
|
|
1295
1383
|
let row = index / columns
|
|
1296
1384
|
let column = index % columns
|
|
1297
|
-
let rowHeight = rowHeights[row]
|
|
1385
|
+
let rowHeight = rowHeights[row] + rowExtra
|
|
1298
1386
|
let size = sizes[index]
|
|
1299
1387
|
let align = style.alignSelf ?? alignItems
|
|
1300
1388
|
let availableWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1301
1389
|
let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
|
|
1302
1390
|
let childWidth = min(availableWidth, size.width)
|
|
1303
|
-
let childHeight = align == "stretch" && style.height == nil ? availableHeight : min(availableHeight, size.height)
|
|
1391
|
+
let childHeight = align == "stretch" && style.height == nil && style.aspectRatio == nil ? availableHeight : min(availableHeight, size.height)
|
|
1304
1392
|
let x = padding.left + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
|
|
1305
1393
|
+ (align == "center" ? (availableWidth - childWidth) / 2 : align == "flex-end" ? availableWidth - childWidth : 0)
|
|
1306
1394
|
let y = rowTop + style.marginTop
|
|
1307
1395
|
+ (align == "center" ? (availableHeight - childHeight) / 2 : align == "flex-end" ? availableHeight - childHeight : 0)
|
|
1308
1396
|
frames.append((child, CGRect(x: x, y: y, width: childWidth, height: childHeight)))
|
|
1309
|
-
if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight +
|
|
1397
|
+
if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + rowBetween }
|
|
1310
1398
|
}
|
|
1311
1399
|
}
|
|
1312
1400
|
return CGSize(width: width, height: innerHeight ?? height)
|
|
@@ -3581,7 +3669,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3581
3669
|
current.protocolId = node["id"] as? String
|
|
3582
3670
|
current.style = style
|
|
3583
3671
|
result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
|
|
3584
|
-
if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "FlatList" && type != "Modal" {
|
|
3672
|
+
if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "SectionList" && type != "FlatList" && type != "Modal" {
|
|
3585
3673
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
3586
3674
|
}
|
|
3587
3675
|
applyViewStyle(style, to: result, node: current)
|
|
@@ -3893,7 +3981,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3893
3981
|
controls: (props["controls"] as? Bool) == true,
|
|
3894
3982
|
resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
3895
3983
|
)
|
|
3896
|
-
case "ScrollView":
|
|
3984
|
+
case "ScrollView", "SectionList":
|
|
3897
3985
|
let scroll = result as! CraftNativeScrollView
|
|
3898
3986
|
let flexDirection = style["flexDirection"] as? String
|
|
3899
3987
|
let direction = (props["horizontal"] as? Bool) == true
|
|
@@ -4020,7 +4108,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4020
4108
|
let icon = CraftNativeIconView()
|
|
4021
4109
|
icon.contentMode = .scaleAspectFit
|
|
4022
4110
|
return icon
|
|
4023
|
-
case "ScrollView":
|
|
4111
|
+
case "ScrollView", "SectionList":
|
|
4024
4112
|
return CraftNativeScrollView()
|
|
4025
4113
|
case "FlatList":
|
|
4026
4114
|
return CraftNativeFlatList()
|
|
@@ -4055,6 +4143,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4055
4143
|
case "video": return "Video"
|
|
4056
4144
|
case "icon": return "Icon"
|
|
4057
4145
|
case "scrollview": return "ScrollView"
|
|
4146
|
+
case "sectionlist": return "SectionList"
|
|
4058
4147
|
case "flatlist": return "FlatList"
|
|
4059
4148
|
case "svg": return "Svg"
|
|
4060
4149
|
case "touchableopacity": return "TouchableOpacity"
|
package/dist/mobile.js
CHANGED
|
@@ -1348,6 +1348,18 @@ var health = {
|
|
|
1348
1348
|
if (!craft?.health?.getSleepIntervals)
|
|
1349
1349
|
throw new Error("Reading sleep intervals is unavailable");
|
|
1350
1350
|
return craft.health.getSleepIntervals(options);
|
|
1351
|
+
},
|
|
1352
|
+
async saveMindfulSession(session) {
|
|
1353
|
+
const craft = getCraftRoot();
|
|
1354
|
+
if (!craft?.health?.saveMindfulSession)
|
|
1355
|
+
throw new Error("Saving mindful sessions is unavailable");
|
|
1356
|
+
return craft.health.saveMindfulSession(session);
|
|
1357
|
+
},
|
|
1358
|
+
async getMindfulSessions(options = {}) {
|
|
1359
|
+
const craft = getCraftRoot();
|
|
1360
|
+
if (!craft?.health?.getMindfulSessions)
|
|
1361
|
+
throw new Error("Reading mindful sessions is unavailable");
|
|
1362
|
+
return craft.health.getMindfulSessions(options);
|
|
1351
1363
|
}
|
|
1352
1364
|
};
|
|
1353
1365
|
async function updateLiveActivity(idOrState, state) {
|