craft-native 0.0.160 → 0.0.161
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 +32 -7
- package/dist/api/mobile.d.ts +55 -4
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +24 -0
- package/dist/ios/templates/CraftHealth.swift +112 -0
- package/dist/ios/templates/CraftNativeScreen.swift +101 -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)
|
|
@@ -772,6 +776,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
772
776
|
listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
|
|
773
777
|
}.toFloatArray(),
|
|
774
778
|
gridAutoRows?.resolve(contentHeight)?.toFloat(),
|
|
779
|
+
alignContentCode(),
|
|
775
780
|
)
|
|
776
781
|
if (sharedFrames != null) {
|
|
777
782
|
for ((index, entry) in flow.withIndex()) {
|
|
@@ -783,9 +788,26 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
783
788
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
784
789
|
}
|
|
785
790
|
} else {
|
|
786
|
-
|
|
791
|
+
val totalRows = layout.rowHeights.sum()
|
|
792
|
+
val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
|
|
793
|
+
val freeRows = max(0, contentHeight - totalRows - rowGaps)
|
|
794
|
+
val rowCount = max(1, layout.rowHeights.size)
|
|
795
|
+
var rowLeading = 0
|
|
796
|
+
var rowBetween = rowGap
|
|
797
|
+
var rowExtra = 0
|
|
798
|
+
when (alignContentCode()) {
|
|
799
|
+
1 -> rowLeading = freeRows / 2
|
|
800
|
+
2 -> rowLeading = freeRows
|
|
801
|
+
3 -> if (rowCount > 1) rowBetween += freeRows / (rowCount - 1)
|
|
802
|
+
4 -> {
|
|
803
|
+
rowLeading = freeRows / (rowCount * 2)
|
|
804
|
+
rowBetween += freeRows / rowCount
|
|
805
|
+
}
|
|
806
|
+
5 -> rowExtra = freeRows / rowCount
|
|
807
|
+
}
|
|
808
|
+
var rowTop = paddingTop + rowLeading
|
|
787
809
|
for ((rowIndex, row) in layout.rows.withIndex()) {
|
|
788
|
-
val rowHeight = layout.rowHeights[rowIndex]
|
|
810
|
+
val rowHeight = layout.rowHeights[rowIndex] + rowExtra
|
|
789
811
|
for ((column, entry) in row.withIndex()) {
|
|
790
812
|
val align = entry.style.alignSelf ?: alignItems
|
|
791
813
|
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
@@ -809,7 +831,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
809
831
|
}
|
|
810
832
|
entry.view.layout(x, y, x + childWidth, y + childHeight)
|
|
811
833
|
}
|
|
812
|
-
rowTop += rowHeight +
|
|
834
|
+
rowTop += rowHeight + rowBetween
|
|
813
835
|
}
|
|
814
836
|
}
|
|
815
837
|
for (entry in layout.absolute) {
|
|
@@ -2803,7 +2825,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
2803
2825
|
"Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
|
|
2804
2826
|
drawing = node
|
|
2805
2827
|
}
|
|
2806
|
-
"ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2828
|
+
"ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
2807
2829
|
val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
|
|
2808
2830
|
setHorizontal(horizontal)
|
|
2809
2831
|
setNativeNestedScrollingEnabled(true)
|
|
@@ -3064,6 +3086,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3064
3086
|
"video" -> "Video"
|
|
3065
3087
|
"icon" -> "Icon"
|
|
3066
3088
|
"scrollview" -> "ScrollView"
|
|
3089
|
+
"sectionlist" -> "SectionList"
|
|
3067
3090
|
"flatlist" -> "FlatList"
|
|
3068
3091
|
"svg" -> "Svg"
|
|
3069
3092
|
"touchableopacity" -> "TouchableOpacity"
|
|
@@ -3477,7 +3500,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3477
3500
|
else -> null
|
|
3478
3501
|
}
|
|
3479
3502
|
view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
|
|
3480
|
-
val role = props.optString("accessibilityRole").ifBlank {
|
|
3503
|
+
val role = props.optString("accessibilityRole").ifBlank {
|
|
3504
|
+
if (type == "Icon") "image" else type.lowercase()
|
|
3505
|
+
}
|
|
3481
3506
|
val state = props.optJSONObject("accessibilityState")
|
|
3482
3507
|
val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
|
|
3483
3508
|
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.161";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -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,17 @@ 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
|
+
default: return 0
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
238
270
|
private final class CraftNativeVideoView: UIView {
|
|
239
271
|
private let playerController = AVPlayerViewController()
|
|
240
272
|
private var player: AVPlayer?
|
|
@@ -436,6 +468,7 @@ private enum CraftNativeLength: Equatable {
|
|
|
436
468
|
private struct CraftNativeLayoutStyle {
|
|
437
469
|
let width: CraftNativeLength?
|
|
438
470
|
let height: CraftNativeLength?
|
|
471
|
+
let aspectRatio: CGFloat?
|
|
439
472
|
let minWidth: CraftNativeLength?
|
|
440
473
|
let maxWidth: CraftNativeLength?
|
|
441
474
|
let minHeight: CraftNativeLength?
|
|
@@ -465,6 +498,8 @@ private struct CraftNativeLayoutStyle {
|
|
|
465
498
|
func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
|
|
466
499
|
width = CraftNativeLength(raw["width"])
|
|
467
500
|
height = CraftNativeLength(raw["height"])
|
|
501
|
+
let ratio = number(raw["aspectRatio"])
|
|
502
|
+
aspectRatio = ratio.flatMap { $0.isFinite && $0 > 0 ? $0 : nil }
|
|
468
503
|
minWidth = CraftNativeLength(raw["minWidth"])
|
|
469
504
|
maxWidth = CraftNativeLength(raw["maxWidth"])
|
|
470
505
|
minHeight = CraftNativeLength(raw["minHeight"])
|
|
@@ -870,12 +905,13 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
870
905
|
let content = CGRect(x: padding.left, y: padding.top, width: max(0, size.width - paddingWidth), height: max(0, size.height - paddingHeight))
|
|
871
906
|
for child in children where child.craftLayoutStyle.position == "absolute" {
|
|
872
907
|
let style = child.craftLayoutStyle
|
|
873
|
-
|
|
908
|
+
var fixedWidth = style.left != nil && style.right != nil
|
|
874
909
|
? max(0, content.width - (style.left ?? 0) - (style.right ?? 0))
|
|
875
910
|
: style.width?.resolve(content.width)
|
|
876
|
-
|
|
911
|
+
var fixedHeight = style.top != nil && style.bottom != nil
|
|
877
912
|
? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0))
|
|
878
913
|
: style.height?.resolve(content.height)
|
|
914
|
+
(fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
|
|
879
915
|
let measured = CraftNativeFlexLayout.measure(child, width: fixedWidth, height: fixedHeight, maxWidth: content.width, maxHeight: content.height)
|
|
880
916
|
let x = style.left ?? (style.right.map { content.width - $0 - measured.width } ?? 0)
|
|
881
917
|
let y = style.top ?? (style.bottom.map { content.height - $0 - measured.height } ?? 0)
|
|
@@ -885,6 +921,21 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
885
921
|
return (size, frames)
|
|
886
922
|
}
|
|
887
923
|
|
|
924
|
+
/// Resolves the missing side of an aspect-ratio box after its parent has
|
|
925
|
+
/// supplied any definite width or height. Keeping this at the host edge
|
|
926
|
+
/// lets grid and absolute children follow the same rule as flex items
|
|
927
|
+
/// without adding platform-specific measurement behavior to native views.
|
|
928
|
+
private func aspectDimensions(
|
|
929
|
+
_ style: CraftNativeLayoutStyle,
|
|
930
|
+
width: CGFloat?,
|
|
931
|
+
height: CGFloat?
|
|
932
|
+
) -> (width: CGFloat?, height: CGFloat?) {
|
|
933
|
+
guard let ratio = style.aspectRatio else { return (width, height) }
|
|
934
|
+
if let width, height == nil { return (width, width / ratio) }
|
|
935
|
+
if let height, width == nil { return (height * ratio, height) }
|
|
936
|
+
return (width, height)
|
|
937
|
+
}
|
|
938
|
+
|
|
888
939
|
private func layoutFlex(
|
|
889
940
|
_ children: [UIView],
|
|
890
941
|
horizontal: Bool,
|
|
@@ -964,8 +1015,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
964
1015
|
let crossLeading = horizontal ? style.marginTop : style.marginLeft
|
|
965
1016
|
let crossTrailing = horizontal ? style.marginBottom : style.marginRight
|
|
966
1017
|
let align = style.alignSelf ?? alignItems
|
|
967
|
-
let
|
|
968
|
-
let
|
|
1018
|
+
let authoredMain = (horizontal ? style.width : style.height)?.resolve(definiteMain)
|
|
1019
|
+
let authoredCross = (horizontal ? style.height : style.width)?.resolve(definiteCross)
|
|
1020
|
+
let explicitMain = authoredMain ?? style.aspectRatio.flatMap { ratio in
|
|
1021
|
+
authoredCross.map { horizontal ? $0 * ratio : $0 / ratio }
|
|
1022
|
+
}
|
|
1023
|
+
let explicitCross = authoredCross ?? style.aspectRatio.flatMap { ratio in
|
|
1024
|
+
authoredMain.map { horizontal ? $0 / ratio : $0 * ratio }
|
|
1025
|
+
}
|
|
969
1026
|
let minMain = (horizontal ? style.minWidth : style.minHeight)?.resolve(definiteMain)
|
|
970
1027
|
let maxMain = (horizontal ? style.maxWidth : style.maxHeight)?.resolve(definiteMain)
|
|
971
1028
|
let minCross = (horizontal ? style.minHeight : style.minWidth)?.resolve(definiteCross)
|
|
@@ -1242,11 +1299,14 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1242
1299
|
let style = child.craftLayoutStyle
|
|
1243
1300
|
let available = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1244
1301
|
let align = style.alignSelf ?? alignItems
|
|
1245
|
-
|
|
1302
|
+
var fixedWidth = style.width?.resolve(cellWidth)
|
|
1303
|
+
var fixedHeight = style.height?.resolve(nil)
|
|
1304
|
+
if fixedWidth == nil && fixedHeight == nil && align == "stretch" { fixedWidth = available }
|
|
1305
|
+
(fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
|
|
1246
1306
|
let size = CraftNativeFlexLayout.measure(
|
|
1247
1307
|
child,
|
|
1248
1308
|
width: fixedWidth.map { min($0, available) },
|
|
1249
|
-
height:
|
|
1309
|
+
height: fixedHeight,
|
|
1250
1310
|
maxWidth: available,
|
|
1251
1311
|
maxHeight: CraftNativeFlexLayout.unbounded
|
|
1252
1312
|
)
|
|
@@ -1270,16 +1330,23 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1270
1330
|
let align = style.alignSelf ?? alignItems
|
|
1271
1331
|
let size = sizes[index]
|
|
1272
1332
|
let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
|
|
1333
|
+
var explicitWidth = style.width?.resolve(cellWidth)
|
|
1334
|
+
var explicitHeight = style.height?.resolve(nil)
|
|
1335
|
+
if explicitWidth == nil && explicitHeight == nil && align == "stretch" {
|
|
1336
|
+
explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1337
|
+
}
|
|
1338
|
+
(explicitWidth, explicitHeight) = aspectDimensions(style, width: explicitWidth, height: explicitHeight)
|
|
1273
1339
|
return (
|
|
1274
1340
|
size.width, size.height,
|
|
1275
|
-
|
|
1341
|
+
explicitWidth, explicitHeight,
|
|
1276
1342
|
style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
|
|
1277
1343
|
style.minHeight?.resolve(nil), style.maxHeight?.resolve(nil),
|
|
1278
1344
|
style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
|
|
1279
1345
|
align == "stretch", alignment
|
|
1280
1346
|
)
|
|
1281
1347
|
},
|
|
1282
|
-
autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 }
|
|
1348
|
+
autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 },
|
|
1349
|
+
alignContent: craftNativeAlignContentCode(alignContent)
|
|
1283
1350
|
)
|
|
1284
1351
|
if place {
|
|
1285
1352
|
if let sharedFrames {
|
|
@@ -1289,24 +1356,41 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1289
1356
|
}
|
|
1290
1357
|
return CGSize(width: width, height: innerHeight ?? height)
|
|
1291
1358
|
}
|
|
1292
|
-
|
|
1359
|
+
let gridHeight = innerHeight ?? height
|
|
1360
|
+
let gridGaps = CGFloat(max(0, rowHeights.count - 1)) * verticalGap
|
|
1361
|
+
let gridFree = max(0, gridHeight - rowHeights.reduce(0, +) - gridGaps)
|
|
1362
|
+
var rowLeading: CGFloat = 0
|
|
1363
|
+
var rowBetween = verticalGap
|
|
1364
|
+
var rowExtra: CGFloat = 0
|
|
1365
|
+
switch alignContent {
|
|
1366
|
+
case "center": rowLeading = gridFree / 2
|
|
1367
|
+
case "flex-end": rowLeading = gridFree
|
|
1368
|
+
case "space-between":
|
|
1369
|
+
if rowHeights.count > 1 { rowBetween += gridFree / CGFloat(rowHeights.count - 1) }
|
|
1370
|
+
case "space-around":
|
|
1371
|
+
rowLeading = gridFree / CGFloat(rowHeights.count * 2)
|
|
1372
|
+
rowBetween += gridFree / CGFloat(rowHeights.count)
|
|
1373
|
+
case "stretch": rowExtra = gridFree / CGFloat(max(1, rowHeights.count))
|
|
1374
|
+
default: break
|
|
1375
|
+
}
|
|
1376
|
+
var rowTop = padding.top + rowLeading
|
|
1293
1377
|
for (index, child) in children.enumerated() {
|
|
1294
1378
|
let style = child.craftLayoutStyle
|
|
1295
1379
|
let row = index / columns
|
|
1296
1380
|
let column = index % columns
|
|
1297
|
-
let rowHeight = rowHeights[row]
|
|
1381
|
+
let rowHeight = rowHeights[row] + rowExtra
|
|
1298
1382
|
let size = sizes[index]
|
|
1299
1383
|
let align = style.alignSelf ?? alignItems
|
|
1300
1384
|
let availableWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
|
|
1301
1385
|
let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
|
|
1302
1386
|
let childWidth = min(availableWidth, size.width)
|
|
1303
|
-
let childHeight = align == "stretch" && style.height == nil ? availableHeight : min(availableHeight, size.height)
|
|
1387
|
+
let childHeight = align == "stretch" && style.height == nil && style.aspectRatio == nil ? availableHeight : min(availableHeight, size.height)
|
|
1304
1388
|
let x = padding.left + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
|
|
1305
1389
|
+ (align == "center" ? (availableWidth - childWidth) / 2 : align == "flex-end" ? availableWidth - childWidth : 0)
|
|
1306
1390
|
let y = rowTop + style.marginTop
|
|
1307
1391
|
+ (align == "center" ? (availableHeight - childHeight) / 2 : align == "flex-end" ? availableHeight - childHeight : 0)
|
|
1308
1392
|
frames.append((child, CGRect(x: x, y: y, width: childWidth, height: childHeight)))
|
|
1309
|
-
if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight +
|
|
1393
|
+
if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + rowBetween }
|
|
1310
1394
|
}
|
|
1311
1395
|
}
|
|
1312
1396
|
return CGSize(width: width, height: innerHeight ?? height)
|
|
@@ -3581,7 +3665,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3581
3665
|
current.protocolId = node["id"] as? String
|
|
3582
3666
|
current.style = style
|
|
3583
3667
|
result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
|
|
3584
|
-
if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "FlatList" && type != "Modal" {
|
|
3668
|
+
if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "SectionList" && type != "FlatList" && type != "Modal" {
|
|
3585
3669
|
result.setContentHuggingPriority(.required, for: .vertical)
|
|
3586
3670
|
}
|
|
3587
3671
|
applyViewStyle(style, to: result, node: current)
|
|
@@ -3893,7 +3977,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3893
3977
|
controls: (props["controls"] as? Bool) == true,
|
|
3894
3978
|
resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
3895
3979
|
)
|
|
3896
|
-
case "ScrollView":
|
|
3980
|
+
case "ScrollView", "SectionList":
|
|
3897
3981
|
let scroll = result as! CraftNativeScrollView
|
|
3898
3982
|
let flexDirection = style["flexDirection"] as? String
|
|
3899
3983
|
let direction = (props["horizontal"] as? Bool) == true
|
|
@@ -4020,7 +4104,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4020
4104
|
let icon = CraftNativeIconView()
|
|
4021
4105
|
icon.contentMode = .scaleAspectFit
|
|
4022
4106
|
return icon
|
|
4023
|
-
case "ScrollView":
|
|
4107
|
+
case "ScrollView", "SectionList":
|
|
4024
4108
|
return CraftNativeScrollView()
|
|
4025
4109
|
case "FlatList":
|
|
4026
4110
|
return CraftNativeFlatList()
|
|
@@ -4055,6 +4139,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4055
4139
|
case "video": return "Video"
|
|
4056
4140
|
case "icon": return "Icon"
|
|
4057
4141
|
case "scrollview": return "ScrollView"
|
|
4142
|
+
case "sectionlist": return "SectionList"
|
|
4058
4143
|
case "flatlist": return "FlatList"
|
|
4059
4144
|
case "svg": return "Svg"
|
|
4060
4145
|
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) {
|