craft-native 0.0.194 → 0.0.196
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/api/mobile.d.ts +21 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftActivityAttributes.swift +12 -0
- package/dist/ios/templates/CraftApp.swift +19 -111
- package/dist/ios/templates/CraftLiveActivities.swift +98 -0
- package/dist/ios/templates/CraftLiveActivityWidget.swift.template +77 -24
- package/dist/ios/templates/CraftNativeActions.swift +6 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +13 -0
- package/dist/ios/templates/CraftNativeScreen.swift +1 -0
- package/dist/mobile.js +6 -0
- package/package.json +1 -1
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1632,12 +1632,27 @@ export declare const health: {
|
|
|
1632
1632
|
onHeartRate(callback: (samples: HealthHeartRateSample[]) => void): () => void;
|
|
1633
1633
|
};
|
|
1634
1634
|
export interface LiveActivityState {
|
|
1635
|
+
/** What to show once staleAt passes without fresh app state. */
|
|
1636
|
+
staleStatus?: string;
|
|
1637
|
+
/** Current step, interval or session detail. */
|
|
1638
|
+
detail?: string;
|
|
1639
|
+
/** Epoch milliseconds; system-rendered timers keep advancing while the app sleeps. */
|
|
1640
|
+
timerStartedAt?: number | null;
|
|
1641
|
+
timerEndsAt?: number | null;
|
|
1642
|
+
paused?: boolean;
|
|
1643
|
+
remainingSeconds?: number;
|
|
1644
|
+
heartRate?: number | null;
|
|
1645
|
+
showsDistance?: boolean;
|
|
1646
|
+
staleAt?: number | null;
|
|
1635
1647
|
status?: string;
|
|
1636
1648
|
distanceMeters?: number;
|
|
1637
1649
|
durationSeconds?: number;
|
|
1638
1650
|
progress?: number;
|
|
1639
1651
|
}
|
|
1640
1652
|
export interface LiveActivityOptions extends LiveActivityState {
|
|
1653
|
+
symbol?: string;
|
|
1654
|
+
deepLink?: string;
|
|
1655
|
+
tint?: string;
|
|
1641
1656
|
activityId: string;
|
|
1642
1657
|
title: string;
|
|
1643
1658
|
}
|
|
@@ -1647,6 +1662,12 @@ export interface LiveActivityHandle {
|
|
|
1647
1662
|
end(finalState?: LiveActivityState): Promise<void>;
|
|
1648
1663
|
}
|
|
1649
1664
|
export interface LiveActivitiesApi {
|
|
1665
|
+
isAvailable(): Promise<boolean>;
|
|
1666
|
+
list(): Promise<Array<{
|
|
1667
|
+
id: string;
|
|
1668
|
+
activityId: string;
|
|
1669
|
+
title: string;
|
|
1670
|
+
}>>;
|
|
1650
1671
|
start(options: LiveActivityOptions): Promise<LiveActivityHandle>;
|
|
1651
1672
|
update(id: string, state: LiveActivityState): Promise<void>;
|
|
1652
1673
|
/** @deprecated Pass the activity id returned by start, or use handle.update(state). */
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.196";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -1018,6 +1018,7 @@ function writeShell(output, config) {
|
|
|
1018
1018
|
cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
|
|
1019
1019
|
cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
|
|
1020
1020
|
writeFileSync2(join3(output, "Sources", "CraftCrashReporter.swift"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
|
|
1021
|
+
cpSync(join3(TEMPLATES_DIR, "CraftLiveActivities.swift"), join3(output, "Sources", "CraftLiveActivities.swift"));
|
|
1021
1022
|
cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
|
|
1022
1023
|
for (const [enabled, folder, template, filename] of [
|
|
1023
1024
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
@@ -7,8 +7,20 @@ struct CraftActivityAttributes: ActivityAttributes {
|
|
|
7
7
|
let distanceMeters: Double
|
|
8
8
|
let durationSeconds: Double
|
|
9
9
|
let progress: Double
|
|
10
|
+
var staleStatus: String?
|
|
11
|
+
var detail: String?
|
|
12
|
+
/// Epoch milliseconds. SwiftUI advances these timers without app updates.
|
|
13
|
+
var timerStartedAt: Double?
|
|
14
|
+
var timerEndsAt: Double?
|
|
15
|
+
var paused: Bool?
|
|
16
|
+
var remainingSeconds: Double?
|
|
17
|
+
var heartRate: Double?
|
|
18
|
+
var showsDistance: Bool?
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
let activityId: String
|
|
13
22
|
let title: String
|
|
23
|
+
var symbol: String?
|
|
24
|
+
var deepLink: String?
|
|
25
|
+
var tint: String?
|
|
14
26
|
}
|
|
@@ -2752,6 +2752,10 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2752
2752
|
}
|
|
2753
2753
|
|
|
2754
2754
|
// MARK: - Live Activities
|
|
2755
|
+
case "isLiveActivityAvailable":
|
|
2756
|
+
liveActivity(method: "isAvailable", args: [], callbackId: callbackId)
|
|
2757
|
+
case "listLiveActivities":
|
|
2758
|
+
liveActivity(method: "list", args: [], callbackId: callbackId)
|
|
2755
2759
|
case "startLiveActivity":
|
|
2756
2760
|
if config.enableLiveActivities {
|
|
2757
2761
|
startLiveActivity(body: body, callbackId: callbackId)
|
|
@@ -5380,6 +5384,8 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5380
5384
|
};
|
|
5381
5385
|
})();
|
|
5382
5386
|
craft.liveActivity = {
|
|
5387
|
+
isAvailable: function() { return craft._invoke('isLiveActivityAvailable'); },
|
|
5388
|
+
list: function() { return craft._invoke('listLiveActivities'); },
|
|
5383
5389
|
start: function(options) { return craft._invoke('startLiveActivity', options || {}); },
|
|
5384
5390
|
update: function(idOrState, state) {
|
|
5385
5391
|
if (typeof idOrState !== 'string') return craft._invoke('updateLiveActivity', idOrState || {});
|
|
@@ -7463,123 +7469,25 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
7463
7469
|
}
|
|
7464
7470
|
|
|
7465
7471
|
// MARK: - Live Activities
|
|
7466
|
-
private func
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
return
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
)
|
|
7475
|
-
let state = CraftActivityAttributes.ContentState(
|
|
7476
|
-
status: body["status"] as? String ?? "Recording",
|
|
7477
|
-
distanceMeters: body["distanceMeters"] as? Double ?? 0,
|
|
7478
|
-
durationSeconds: body["durationSeconds"] as? Double ?? 0,
|
|
7479
|
-
progress: min(max(body["progress"] as? Double ?? 0, 0), 1)
|
|
7480
|
-
)
|
|
7481
|
-
do {
|
|
7482
|
-
let activity = try Activity.request(
|
|
7483
|
-
attributes: attributes,
|
|
7484
|
-
content: ActivityContent(state: state, staleDate: nil),
|
|
7485
|
-
pushType: nil
|
|
7486
|
-
)
|
|
7487
|
-
resolveCallback(callbackId, result: ["id": activity.id])
|
|
7488
|
-
} catch {
|
|
7489
|
-
rejectCallback(callbackId, error: error.localizedDescription)
|
|
7472
|
+
private func liveActivity(method: String, args: [Any], callbackId: String?) {
|
|
7473
|
+
let token = armDeadline(Coordinator.liveActivityDeadline, callbackId: callbackId, error: "Live Activity request timed out")
|
|
7474
|
+
CraftLiveActivities.perform(method: method, args: args, enabled: config.enableLiveActivities, appName: config.appName) { [weak self] result in
|
|
7475
|
+
guard let self, self.claimDeadline(token) else { return }
|
|
7476
|
+
switch result {
|
|
7477
|
+
case .success(let value): self.resolveCallback(callbackId, result: value)
|
|
7478
|
+
case .failure(let error): self.rejectCallback(callbackId, error: error.message)
|
|
7479
|
+
}
|
|
7490
7480
|
}
|
|
7491
7481
|
}
|
|
7492
7482
|
|
|
7483
|
+
private func startLiveActivity(body: [String: Any], callbackId: String?) {
|
|
7484
|
+
liveActivity(method: "start", args: [body], callbackId: callbackId)
|
|
7485
|
+
}
|
|
7493
7486
|
private func updateLiveActivity(body: [String: Any], callbackId: String?) {
|
|
7494
|
-
|
|
7495
|
-
rejectCallback(callbackId, error: "No Live Activity is running")
|
|
7496
|
-
return
|
|
7497
|
-
}
|
|
7498
|
-
let activities = Activity<CraftActivityAttributes>.activities
|
|
7499
|
-
let activity: Activity<CraftActivityAttributes>
|
|
7500
|
-
if let activityId = body["id"] as? String {
|
|
7501
|
-
guard let matchingActivity = activities.first(where: { $0.id == activityId }) else {
|
|
7502
|
-
rejectCallback(callbackId, error: "No Live Activity with id \(activityId) is running")
|
|
7503
|
-
return
|
|
7504
|
-
}
|
|
7505
|
-
activity = matchingActivity
|
|
7506
|
-
} else {
|
|
7507
|
-
guard let currentActivity = activities.first else {
|
|
7508
|
-
rejectCallback(callbackId, error: "No Live Activity is running")
|
|
7509
|
-
return
|
|
7510
|
-
}
|
|
7511
|
-
activity = currentActivity
|
|
7512
|
-
}
|
|
7513
|
-
let current = activity.content.state
|
|
7514
|
-
let state = CraftActivityAttributes.ContentState(
|
|
7515
|
-
status: body["status"] as? String ?? current.status,
|
|
7516
|
-
distanceMeters: body["distanceMeters"] as? Double ?? current.distanceMeters,
|
|
7517
|
-
durationSeconds: body["durationSeconds"] as? Double ?? current.durationSeconds,
|
|
7518
|
-
progress: min(max(body["progress"] as? Double ?? current.progress, 0), 1)
|
|
7519
|
-
)
|
|
7520
|
-
let token = armDeadline(
|
|
7521
|
-
Coordinator.liveActivityDeadline,
|
|
7522
|
-
callbackId: callbackId,
|
|
7523
|
-
error: "Activity.update did not return within \(Int(Coordinator.liveActivityDeadline))s"
|
|
7524
|
-
)
|
|
7525
|
-
Task {
|
|
7526
|
-
await activity.update(ActivityContent(state: state, staleDate: nil))
|
|
7527
|
-
await MainActor.run { [weak self] in
|
|
7528
|
-
guard let self, self.claimDeadline(token) else { return }
|
|
7529
|
-
self.resolveCallback(callbackId, result: ["updated": true])
|
|
7530
|
-
}
|
|
7531
|
-
}
|
|
7487
|
+
liveActivity(method: "update", args: [body["id"] ?? NSNull(), body], callbackId: callbackId)
|
|
7532
7488
|
}
|
|
7533
|
-
|
|
7534
7489
|
private func endLiveActivity(body: [String: Any], callbackId: String?) {
|
|
7535
|
-
|
|
7536
|
-
resolveCallback(callbackId, result: ["ended": false])
|
|
7537
|
-
return
|
|
7538
|
-
}
|
|
7539
|
-
let activities = Activity<CraftActivityAttributes>.activities
|
|
7540
|
-
let activity: Activity<CraftActivityAttributes>
|
|
7541
|
-
if let activityId = body["id"] as? String {
|
|
7542
|
-
guard let matchingActivity = activities.first(where: { $0.id == activityId }) else {
|
|
7543
|
-
rejectCallback(callbackId, error: "No Live Activity with id \(activityId) is running")
|
|
7544
|
-
return
|
|
7545
|
-
}
|
|
7546
|
-
activity = matchingActivity
|
|
7547
|
-
} else {
|
|
7548
|
-
guard let currentActivity = activities.first else {
|
|
7549
|
-
resolveCallback(callbackId, result: ["ended": false])
|
|
7550
|
-
return
|
|
7551
|
-
}
|
|
7552
|
-
activity = currentActivity
|
|
7553
|
-
}
|
|
7554
|
-
let hasFinalState = body["status"] != nil
|
|
7555
|
-
|| body["distanceMeters"] != nil
|
|
7556
|
-
|| body["durationSeconds"] != nil
|
|
7557
|
-
|| body["progress"] != nil
|
|
7558
|
-
let finalContent: ActivityContent<CraftActivityAttributes.ContentState>?
|
|
7559
|
-
if hasFinalState {
|
|
7560
|
-
let current = activity.content.state
|
|
7561
|
-
let state = CraftActivityAttributes.ContentState(
|
|
7562
|
-
status: body["status"] as? String ?? current.status,
|
|
7563
|
-
distanceMeters: body["distanceMeters"] as? Double ?? current.distanceMeters,
|
|
7564
|
-
durationSeconds: body["durationSeconds"] as? Double ?? current.durationSeconds,
|
|
7565
|
-
progress: min(max(body["progress"] as? Double ?? current.progress, 0), 1)
|
|
7566
|
-
)
|
|
7567
|
-
finalContent = ActivityContent(state: state, staleDate: nil)
|
|
7568
|
-
} else {
|
|
7569
|
-
finalContent = nil
|
|
7570
|
-
}
|
|
7571
|
-
let token = armDeadline(
|
|
7572
|
-
Coordinator.liveActivityDeadline,
|
|
7573
|
-
callbackId: callbackId,
|
|
7574
|
-
error: "Activity.end did not return within \(Int(Coordinator.liveActivityDeadline))s"
|
|
7575
|
-
)
|
|
7576
|
-
Task {
|
|
7577
|
-
await activity.end(finalContent, dismissalPolicy: .default)
|
|
7578
|
-
await MainActor.run { [weak self] in
|
|
7579
|
-
guard let self, self.claimDeadline(token) else { return }
|
|
7580
|
-
self.resolveCallback(callbackId, result: ["ended": true])
|
|
7581
|
-
}
|
|
7582
|
-
}
|
|
7490
|
+
liveActivity(method: "end", args: [body["id"] ?? NSNull(), body], callbackId: callbackId)
|
|
7583
7491
|
}
|
|
7584
7492
|
|
|
7585
7493
|
// MARK: - Screen Capture
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import ActivityKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// ActivityKit belongs to the process, not a web view or a retained STX screen.
|
|
5
|
+
/// A stable application activityId reattaches after a screen or app restart.
|
|
6
|
+
enum CraftLiveActivities {
|
|
7
|
+
static func perform(method: String, args: [Any], enabled: Bool, appName: String, completion: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
|
|
8
|
+
guard enabled else {
|
|
9
|
+
if method == "isAvailable" { completion(.success(false)); return }
|
|
10
|
+
completion(.failure(CraftNativeActionError(code: "CAPABILITY_DISABLED", message: "Live Activities are disabled")))
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
guard #available(iOS 16.2, *) else {
|
|
14
|
+
if method == "isAvailable" { completion(.success(false)) }
|
|
15
|
+
else { completion(.failure(CraftNativeActionError(code: "UNSUPPORTED", message: "Live Activities require iOS 16.2"))) }
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
Task { @MainActor in
|
|
19
|
+
do {
|
|
20
|
+
let activities = Activity<CraftActivityAttributes>.activities.filter { $0.activityState == .active || $0.activityState == .stale }
|
|
21
|
+
switch method {
|
|
22
|
+
case "isAvailable": completion(.success(ActivityAuthorizationInfo().areActivitiesEnabled))
|
|
23
|
+
case "list": completion(.success(activities.map { ["id": $0.id, "activityId": $0.attributes.activityId, "title": $0.attributes.title] }))
|
|
24
|
+
case "start":
|
|
25
|
+
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
|
26
|
+
throw CraftNativeActionError(code: "NOT_ALLOWED", message: "Live Activities are turned off in Settings")
|
|
27
|
+
}
|
|
28
|
+
let body = args.first as? [String: Any] ?? [:]
|
|
29
|
+
guard let key = body["activityId"] as? String, !key.isEmpty,
|
|
30
|
+
let title = body["title"] as? String, !title.isEmpty else {
|
|
31
|
+
throw CraftNativeActionError(code: "INVALID_ARGUMENT", message: "A Live Activity needs activityId and title")
|
|
32
|
+
}
|
|
33
|
+
let state = content(body)
|
|
34
|
+
if let existing = activities.first(where: { $0.attributes.activityId == key }) {
|
|
35
|
+
await existing.update(ActivityContent(state: state, staleDate: staleDate(body)))
|
|
36
|
+
completion(.success(["id": existing.id]))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
let attributes = CraftActivityAttributes(activityId: key, title: String(title.prefix(160)), symbol: body["symbol"] as? String, deepLink: safeURL(body["deepLink"] as? String), tint: body["tint"] as? String)
|
|
40
|
+
let activity = try Activity.request(attributes: attributes, content: ActivityContent(state: state, staleDate: staleDate(body)), pushType: nil)
|
|
41
|
+
completion(.success(["id": activity.id]))
|
|
42
|
+
case "update", "end":
|
|
43
|
+
let id = args.first as? String
|
|
44
|
+
let activity = id == nil ? activities.first : activities.first(where: { $0.id == id })
|
|
45
|
+
guard let activity else {
|
|
46
|
+
if method == "end" { completion(.success(["ended": false])); return }
|
|
47
|
+
throw CraftNativeActionError(code: "NOT_FOUND", message: "No matching Live Activity is running")
|
|
48
|
+
}
|
|
49
|
+
let body = args.count > 1 ? args[1] as? [String: Any] ?? [:] : [:]
|
|
50
|
+
let state = content(body, previous: activity.content.state)
|
|
51
|
+
if method == "end" {
|
|
52
|
+
await activity.end(ActivityContent(state: state, staleDate: nil), dismissalPolicy: .immediate)
|
|
53
|
+
completion(.success(["ended": true]))
|
|
54
|
+
} else {
|
|
55
|
+
await activity.update(ActivityContent(state: state, staleDate: staleDate(body)))
|
|
56
|
+
completion(.success(["updated": true]))
|
|
57
|
+
}
|
|
58
|
+
default: throw CraftNativeActionError(code: "UNKNOWN_ACTION", message: "Unsupported Live Activities method")
|
|
59
|
+
}
|
|
60
|
+
} catch let error as CraftNativeActionError { completion(.failure(error)) }
|
|
61
|
+
catch { completion(.failure(CraftNativeActionError(code: "ACTIVITY_ERROR", message: error.localizedDescription))) }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static func safeURL(_ value: String?) -> String? {
|
|
66
|
+
guard let value, let url = URL(string: value), let scheme = url.scheme?.lowercased(),
|
|
67
|
+
!["javascript", "data", "file"].contains(scheme) else { return nil }
|
|
68
|
+
return value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static func finite(_ body: [String: Any], _ key: String, _ fallback: Double? = nil) -> Double? {
|
|
72
|
+
guard body.keys.contains(key) else { return fallback }
|
|
73
|
+
guard let number = body[key] as? NSNumber, number.doubleValue.isFinite else { return nil }
|
|
74
|
+
return max(0, number.doubleValue)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func staleDate(_ body: [String: Any]) -> Date? {
|
|
78
|
+
finite(body, "staleAt").map { Date(timeIntervalSince1970: $0 / 1000) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@available(iOS 16.2, *)
|
|
82
|
+
private static func content(_ body: [String: Any], previous: CraftActivityAttributes.ContentState? = nil) -> CraftActivityAttributes.ContentState {
|
|
83
|
+
CraftActivityAttributes.ContentState(
|
|
84
|
+
status: String((body["status"] as? String ?? previous?.status ?? "").prefix(120)),
|
|
85
|
+
distanceMeters: finite(body, "distanceMeters", previous?.distanceMeters) ?? 0,
|
|
86
|
+
durationSeconds: finite(body, "durationSeconds", previous?.durationSeconds) ?? 0,
|
|
87
|
+
progress: min(finite(body, "progress", previous?.progress) ?? 0, 1),
|
|
88
|
+
staleStatus: body["staleStatus"] as? String ?? previous?.staleStatus,
|
|
89
|
+
detail: body["detail"] as? String ?? previous?.detail,
|
|
90
|
+
timerStartedAt: finite(body, "timerStartedAt", previous?.timerStartedAt),
|
|
91
|
+
timerEndsAt: finite(body, "timerEndsAt", previous?.timerEndsAt),
|
|
92
|
+
paused: body["paused"] as? Bool ?? previous?.paused,
|
|
93
|
+
remainingSeconds: finite(body, "remainingSeconds", previous?.remainingSeconds),
|
|
94
|
+
heartRate: finite(body, "heartRate", previous?.heartRate),
|
|
95
|
+
showsDistance: body["showsDistance"] as? Bool ?? previous?.showsDistance
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -4,59 +4,112 @@ import WidgetKit
|
|
|
4
4
|
|
|
5
5
|
@main
|
|
6
6
|
struct CraftLiveActivityBundle: WidgetBundle {
|
|
7
|
-
var body: some Widget {
|
|
8
|
-
CraftLiveActivityWidget()
|
|
9
|
-
}
|
|
7
|
+
var body: some Widget { CraftLiveActivityWidget() }
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
struct CraftLiveActivityWidget: Widget {
|
|
13
11
|
var body: some WidgetConfiguration {
|
|
14
12
|
ActivityConfiguration(for: CraftActivityAttributes.self) { context in
|
|
15
13
|
HStack(spacing: 12) {
|
|
16
|
-
Image(systemName:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
Image(systemName: symbol(context)).font(.title2).foregroundStyle(tint(context))
|
|
15
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
16
|
+
Text(context.attributes.title).font(.headline).lineLimit(1)
|
|
17
|
+
if let detail = context.state.detail, !detail.isEmpty {
|
|
18
|
+
Text(detail).font(.subheadline).lineLimit(1)
|
|
19
|
+
}
|
|
20
|
+
Text(context.isStale ? context.state.staleStatus ?? context.state.status : context.state.status).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
21
21
|
}
|
|
22
|
-
Spacer()
|
|
23
|
-
VStack(alignment: .trailing, spacing:
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
Spacer(minLength: 8)
|
|
23
|
+
VStack(alignment: .trailing, spacing: 4) {
|
|
24
|
+
timer(context).font(.title2.monospacedDigit().weight(.semibold))
|
|
25
|
+
metrics(context).font(.caption.monospacedDigit())
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
.padding()
|
|
29
|
-
.activityBackgroundTint(Color(red: 0, green: 0.
|
|
29
|
+
.activityBackgroundTint(Color(red: 0.07, green: 0.10, blue: 0.16))
|
|
30
30
|
.activitySystemActionForegroundColor(.white)
|
|
31
|
+
.widgetURL(context.attributes.deepLink.flatMap(URL.init(string:)))
|
|
31
32
|
} dynamicIsland: { context in
|
|
32
33
|
DynamicIsland {
|
|
33
34
|
DynamicIslandExpandedRegion(.leading) {
|
|
34
|
-
|
|
35
|
+
Image(systemName: symbol(context)).foregroundStyle(tint(context)).font(.title2)
|
|
35
36
|
}
|
|
36
37
|
DynamicIslandExpandedRegion(.trailing) {
|
|
37
|
-
|
|
38
|
+
timer(context).font(.title2.monospacedDigit().weight(.semibold))
|
|
39
|
+
}
|
|
40
|
+
DynamicIslandExpandedRegion(.center) {
|
|
41
|
+
VStack(spacing: 3) {
|
|
42
|
+
Text(context.attributes.title).font(.headline).lineLimit(1)
|
|
43
|
+
Text(context.isStale ? context.state.staleStatus ?? context.state.status : context.state.status).font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
DynamicIslandExpandedRegion(.bottom) {
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
VStack(spacing: 8) {
|
|
48
|
+
HStack {
|
|
49
|
+
Text(context.state.detail ?? "").font(.subheadline).lineLimit(1)
|
|
50
|
+
Spacer()
|
|
51
|
+
metrics(context).font(.caption.monospacedDigit())
|
|
52
|
+
}
|
|
53
|
+
if let end = context.state.timerEndsAt, context.state.paused != true, let start = context.state.timerStartedAt, end > start {
|
|
54
|
+
ProgressView(timerInterval: date(start)...date(end), countsDown: false).labelsHidden().tint(tint(context))
|
|
55
|
+
} else {
|
|
56
|
+
ProgressView(value: context.state.progress).tint(tint(context))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
42
59
|
}
|
|
43
60
|
} compactLeading: {
|
|
44
|
-
Image(systemName:
|
|
61
|
+
Image(systemName: symbol(context)).foregroundStyle(tint(context))
|
|
45
62
|
} compactTrailing: {
|
|
46
|
-
|
|
63
|
+
timer(context).monospacedDigit().frame(maxWidth: 64)
|
|
47
64
|
} minimal: {
|
|
48
|
-
Image(systemName:
|
|
65
|
+
Image(systemName: symbol(context)).foregroundStyle(tint(context))
|
|
49
66
|
}
|
|
50
|
-
.
|
|
67
|
+
.widgetURL(context.attributes.deepLink.flatMap(URL.init(string:)))
|
|
68
|
+
.keylineTint(tint(context))
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
private func
|
|
55
|
-
|
|
72
|
+
private func symbol(_ context: ActivityViewContext<CraftActivityAttributes>) -> String {
|
|
73
|
+
context.state.paused == true ? "pause.fill" : context.attributes.symbol ?? "timer"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func tint(_ context: ActivityViewContext<CraftActivityAttributes>) -> Color {
|
|
77
|
+
let raw = context.attributes.tint?.trimmingCharacters(in: CharacterSet(charactersIn: "#")) ?? "007aff"
|
|
78
|
+
guard raw.count == 6, let value = UInt32(raw, radix: 16) else { return .blue }
|
|
79
|
+
return Color(red: Double((value >> 16) & 255) / 255, green: Double((value >> 8) & 255) / 255, blue: Double(value & 255) / 255)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func date(_ milliseconds: Double) -> Date { Date(timeIntervalSince1970: milliseconds / 1000) }
|
|
83
|
+
|
|
84
|
+
@ViewBuilder
|
|
85
|
+
private func timer(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
|
|
86
|
+
if context.state.paused == true {
|
|
87
|
+
Text(duration(context.state.remainingSeconds ?? context.state.durationSeconds))
|
|
88
|
+
} else if let end = context.state.timerEndsAt {
|
|
89
|
+
// Clamped at zero, with no JavaScript timer or per-second Activity.update.
|
|
90
|
+
let start = min(date(context.state.timerStartedAt ?? end - 1), date(end))
|
|
91
|
+
Text(timerInterval: start...date(end), countsDown: true)
|
|
92
|
+
} else if let start = context.state.timerStartedAt {
|
|
93
|
+
Text(date(start), style: .timer)
|
|
94
|
+
} else {
|
|
95
|
+
Text(duration(context.state.durationSeconds))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@ViewBuilder
|
|
100
|
+
private func metrics(_ context: ActivityViewContext<CraftActivityAttributes>) -> some View {
|
|
101
|
+
HStack(spacing: 8) {
|
|
102
|
+
if let bpm = context.state.heartRate, bpm > 0 {
|
|
103
|
+
Label("\(Int(bpm))", systemImage: "heart.fill").foregroundStyle(.pink).privacySensitive()
|
|
104
|
+
}
|
|
105
|
+
if context.state.showsDistance != false {
|
|
106
|
+
Text(Measurement(value: context.state.distanceMeters, unit: UnitLength.meters), format: .measurement(width: .abbreviated, usage: .road)).privacySensitive()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
56
109
|
}
|
|
57
110
|
|
|
58
111
|
private func duration(_ seconds: Double) -> String {
|
|
59
112
|
let value = max(Int(seconds), 0)
|
|
60
|
-
return String(format: "%02d:%02d", value / 60, value % 60)
|
|
113
|
+
return value >= 3600 ? String(format: "%d:%02d:%02d", value / 3600, value / 60 % 60, value % 60) : String(format: "%d:%02d", value / 60, value % 60)
|
|
61
114
|
}
|
|
62
115
|
}
|
|
@@ -48,6 +48,7 @@ enum CraftNativeActions {
|
|
|
48
48
|
if config.enableDeepLinks { values.append("deepLinks") }
|
|
49
49
|
if config.enableLocalNotifications { values.append("notifications") }
|
|
50
50
|
if config.enableHealthKit { values.append("health") }
|
|
51
|
+
if config.enableLiveActivities { values.append("liveActivities") }
|
|
51
52
|
// The in-app browser and the system's web sign-in sheet (CraftBrowser).
|
|
52
53
|
values.append("browser")
|
|
53
54
|
if config.enableSocialAuth { values.append("appleSignIn") }
|
|
@@ -328,6 +329,11 @@ enum CraftNativeActions {
|
|
|
328
329
|
}
|
|
329
330
|
}
|
|
330
331
|
return
|
|
332
|
+
case ("LiveActivities", _):
|
|
333
|
+
CraftLiveActivities.perform(method: method, args: args, enabled: config.enableLiveActivities, appName: config.appName) { result in
|
|
334
|
+
complete(requestToken, result, completion)
|
|
335
|
+
}
|
|
336
|
+
return
|
|
331
337
|
case ("Health", _):
|
|
332
338
|
// Apple Health, the same as a page's `craft.health` (CraftHealth.swift).
|
|
333
339
|
guard config.enableHealthKit else {
|
|
@@ -214,6 +214,19 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
214
214
|
if flowLayout.estimatedItemSize != estimate, bounds.width > 0 {
|
|
215
215
|
flowLayout.estimatedItemSize = estimate
|
|
216
216
|
}
|
|
217
|
+
// Cells can outlive a change to the list's width (including its first
|
|
218
|
+
// constrained parent layout). Self-sizing must use the current extent,
|
|
219
|
+
// or UIKit repeatedly receives a preferred width wider than the list.
|
|
220
|
+
var changedExtent = false
|
|
221
|
+
for case let cell as Cell in visibleCells {
|
|
222
|
+
guard let identity = cell.representedIdentity, let item = itemsByIdentity[identity] else { continue }
|
|
223
|
+
let extent = itemExtent(isChrome: item.isChrome)
|
|
224
|
+
if cell.fittingExtent != extent {
|
|
225
|
+
cell.fittingExtent = extent
|
|
226
|
+
changedExtent = true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if changedExtent { flowLayout.invalidateLayout() }
|
|
217
230
|
super.layoutSubviews()
|
|
218
231
|
craftApplyCornerRadius()
|
|
219
232
|
}
|
|
@@ -5534,6 +5534,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5534
5534
|
scale = 1
|
|
5535
5535
|
}
|
|
5536
5536
|
indicator.indicatorScale = scale
|
|
5537
|
+
applyTransform(style["transform"], to: indicator)
|
|
5537
5538
|
indicator.color = color(props["color"]) ?? .tintColor
|
|
5538
5539
|
indicator.hidesWhenStopped = props["hidesWhenStopped"] as? Bool ?? true
|
|
5539
5540
|
if props["animating"] as? Bool == false {
|
package/dist/mobile.js
CHANGED
|
@@ -1550,6 +1550,12 @@ async function endLiveActivity(id, finalState) {
|
|
|
1550
1550
|
await craft.liveActivity.end();
|
|
1551
1551
|
}
|
|
1552
1552
|
var liveActivities = {
|
|
1553
|
+
async isAvailable() {
|
|
1554
|
+
return Boolean(await getCraftRoot()?.liveActivity?.isAvailable?.());
|
|
1555
|
+
},
|
|
1556
|
+
async list() {
|
|
1557
|
+
return await getCraftRoot()?.liveActivity?.list?.() ?? [];
|
|
1558
|
+
},
|
|
1553
1559
|
async start(options) {
|
|
1554
1560
|
const craft = getCraftRoot();
|
|
1555
1561
|
if (!craft?.liveActivity?.start)
|