craft-native 0.0.436 → 0.0.442
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/LICENSE.md +30 -0
- package/README.md +35 -0
- package/dist/android/src/index.d.ts +8 -0
- package/dist/android/src/index.js +17 -2
- package/dist/android/templates/CraftHealthServices.kt.template +2 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +42 -1
- package/dist/android/templates/CraftNativeMotion.kt.template +269 -0
- package/dist/android/templates/CraftNativeServices.kt.template +6 -1
- package/dist/android/templates/CraftPassiveSteps.kt.template +159 -0
- package/dist/android/templates/CraftRecordingPressure.kt.template +156 -0
- package/dist/android/templates/LocationRecordingService.kt.template +77 -9
- package/dist/android/templates/MainActivityNative.kt.template +129 -1
- package/dist/cli.js +289 -108
- package/dist/components/index.d.ts +25 -0
- package/dist/index.cjs +207 -27
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +321 -140
- package/dist/installer-background.d.ts +2 -0
- package/dist/installer-layout.d.ts +1 -0
- package/dist/ios/src/index.d.ts +10 -0
- package/dist/ios/src/index.js +28 -2
- package/dist/ios/templates/CraftApp.swift +14 -0
- package/dist/ios/templates/CraftHealth.swift +2 -1
- package/dist/ios/templates/CraftHealthBackground.swift +308 -0
- package/dist/ios/templates/CraftNativeActions.swift +153 -0
- package/dist/ios/templates/CraftNativePedometer.swift +181 -0
- package/dist/ios/templates/CraftNativeScreen.swift +76 -0
- package/dist/macos/src/index.js +1 -1
- package/dist/macos/src/toolchain.d.ts +1 -1
- package/dist/macos/templates/CraftMacApplication.swift +124 -39
- package/dist/macos/templates/CraftMacRenderer.swift +111 -33
- package/dist/macos/templates/CraftMacRuntime.swift +270 -81
- package/dist/package.d.ts +40 -0
- package/dist/types.d.ts +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
#if CRAFT_PERMISSION_MOTION
|
|
3
|
+
import CoreMotion
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
struct CraftPedometerSample {
|
|
7
|
+
let startDate: Double
|
|
8
|
+
let endDate: Double
|
|
9
|
+
let steps: Int
|
|
10
|
+
var distanceMeters: Double?
|
|
11
|
+
var cadenceStepsPerSecond: Double?
|
|
12
|
+
var paceSecondsPerMeter: Double?
|
|
13
|
+
var averageActivePaceSecondsPerMeter: Double?
|
|
14
|
+
var floorsAscended: Int?
|
|
15
|
+
var floorsDescended: Int?
|
|
16
|
+
|
|
17
|
+
var payload: [String: Any] {
|
|
18
|
+
var value: [String: Any] = ["state": "sample", "kind": "interval", "startDate": startDate, "endDate": endDate, "steps": steps]
|
|
19
|
+
if let distanceMeters { value["distanceMeters"] = distanceMeters }
|
|
20
|
+
if let cadenceStepsPerSecond { value["cadenceStepsPerSecond"] = cadenceStepsPerSecond }
|
|
21
|
+
if let paceSecondsPerMeter { value["paceSecondsPerMeter"] = paceSecondsPerMeter }
|
|
22
|
+
if let averageActivePaceSecondsPerMeter { value["averageActivePaceSecondsPerMeter"] = averageActivePaceSecondsPerMeter }
|
|
23
|
+
if let floorsAscended { value["floorsAscended"] = floorsAscended }
|
|
24
|
+
if let floorsDescended { value["floorsDescended"] = floorsDescended }
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protocol CraftPedometerSource: AnyObject {
|
|
30
|
+
var availability: [String: Bool] { get }
|
|
31
|
+
var permission: String { get }
|
|
32
|
+
func start(from: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void)
|
|
33
|
+
func query(from: Date, to: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void)
|
|
34
|
+
func stop()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#if CRAFT_PERMISSION_MOTION
|
|
38
|
+
private final class CraftCoreMotionPedometer: CraftPedometerSource {
|
|
39
|
+
private let pedometer = CMPedometer()
|
|
40
|
+
var availability: [String: Bool] {
|
|
41
|
+
["steps": CMPedometer.isStepCountingAvailable(), "distance": CMPedometer.isDistanceAvailable(),
|
|
42
|
+
"floors": CMPedometer.isFloorCountingAvailable(), "cadence": CMPedometer.isCadenceAvailable(),
|
|
43
|
+
"pace": CMPedometer.isPaceAvailable(), "history": CMPedometer.isStepCountingAvailable(), "background": false]
|
|
44
|
+
}
|
|
45
|
+
var permission: String {
|
|
46
|
+
switch CMPedometer.authorizationStatus() {
|
|
47
|
+
case .authorized: return "granted"
|
|
48
|
+
case .denied: return "denied"
|
|
49
|
+
case .restricted: return "restricted"
|
|
50
|
+
case .notDetermined: return "notDetermined"
|
|
51
|
+
@unknown default: return "unavailable"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
private func handler(_ receive: @escaping (Result<CraftPedometerSample, Error>) -> Void) -> CMPedometerHandler {
|
|
55
|
+
{ data, error in
|
|
56
|
+
let result: Result<CraftPedometerSample, Error>
|
|
57
|
+
if let error { result = .failure(error) }
|
|
58
|
+
else if let data {
|
|
59
|
+
result = .success(CraftPedometerSample(startDate: data.startDate.timeIntervalSince1970 * 1000,
|
|
60
|
+
endDate: data.endDate.timeIntervalSince1970 * 1000, steps: data.numberOfSteps.intValue,
|
|
61
|
+
distanceMeters: data.distance?.doubleValue, cadenceStepsPerSecond: data.currentCadence?.doubleValue,
|
|
62
|
+
paceSecondsPerMeter: data.currentPace?.doubleValue, averageActivePaceSecondsPerMeter: data.averageActivePace?.doubleValue,
|
|
63
|
+
floorsAscended: data.floorsAscended?.intValue, floorsDescended: data.floorsDescended?.intValue))
|
|
64
|
+
} else { result = .failure(NSError(domain: "CraftPedometer", code: 1, userInfo: [NSLocalizedDescriptionKey: "Pedometer returned no data"])) }
|
|
65
|
+
DispatchQueue.main.async { receive(result) }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
func start(from: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void) {
|
|
69
|
+
pedometer.startUpdates(from: from, withHandler: handler(receive))
|
|
70
|
+
}
|
|
71
|
+
func query(from: Date, to: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void) {
|
|
72
|
+
pedometer.queryPedometerData(from: from, to: to, withHandler: handler(receive))
|
|
73
|
+
}
|
|
74
|
+
func stop() { pedometer.stopUpdates() }
|
|
75
|
+
}
|
|
76
|
+
#endif
|
|
77
|
+
|
|
78
|
+
private final class CraftUnavailablePedometer: CraftPedometerSource {
|
|
79
|
+
var availability: [String: Bool] { ["steps": false, "distance": false, "floors": false, "cadence": false, "pace": false, "history": false, "background": false] }
|
|
80
|
+
var permission: String { "unavailable" }
|
|
81
|
+
func start(from: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void) {}
|
|
82
|
+
func query(from: Date, to: Date, receive: @escaping (Result<CraftPedometerSample, Error>) -> Void) {}
|
|
83
|
+
func stop() {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Interval counts are never relabelled as Android's counter since boot.
|
|
87
|
+
final class CraftNativePedometer {
|
|
88
|
+
typealias Completion = (Result<Any, CraftNativeActionError>) -> Void
|
|
89
|
+
private let makeSource: () -> CraftPedometerSource
|
|
90
|
+
private let now: () -> Date
|
|
91
|
+
private var live: CraftPedometerSource?
|
|
92
|
+
private var pending = [String: CraftPedometerSource]()
|
|
93
|
+
private var closed = false
|
|
94
|
+
var emit: ([String: Any]) -> Void = { _ in }
|
|
95
|
+
|
|
96
|
+
init(makeSource: (() -> CraftPedometerSource)? = nil, now: @escaping () -> Date = Date.init) {
|
|
97
|
+
self.now = now
|
|
98
|
+
self.makeSource = makeSource ?? {
|
|
99
|
+
#if CRAFT_PERMISSION_MOTION
|
|
100
|
+
return CraftCoreMotionPedometer()
|
|
101
|
+
#else
|
|
102
|
+
return CraftUnavailablePedometer()
|
|
103
|
+
#endif
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func perform(method: String, args: [Any], token: String, completion: @escaping Completion) {
|
|
108
|
+
guard !closed else { return completion(failure("HOST_CLOSED", "Pedometer host is closed")) }
|
|
109
|
+
if method == "stop" { completion(.success(stop(reason: "requested"))); return }
|
|
110
|
+
let source = makeSource()
|
|
111
|
+
if method == "getAvailability" { completion(.success(source.availability)); return }
|
|
112
|
+
if method == "getPermissionStatus" { completion(.success(source.permission)); return }
|
|
113
|
+
guard ["start", "query", "requestPermission"].contains(method) else { return completion(failure("UNKNOWN_ACTION", "Unsupported Pedometer method")) }
|
|
114
|
+
guard source.availability["steps"] == true else { return completion(failure("UNAVAILABLE", "Step counting is not present")) }
|
|
115
|
+
if method == "requestPermission", ["granted", "denied", "restricted"].contains(source.permission) {
|
|
116
|
+
completion(.success(source.permission)); return
|
|
117
|
+
}
|
|
118
|
+
guard !["denied", "restricted"].contains(source.permission) else { return completion(failure("PERMISSION_DENIED", "Motion access was denied or restricted")) }
|
|
119
|
+
guard args.isEmpty || args.first is [String: Any] else { return completion(failure("INVALID_ARGUMENT", "Pedometer options must be an object")) }
|
|
120
|
+
let options = args.first as? [String: Any] ?? [:]
|
|
121
|
+
if let background = options["background"] {
|
|
122
|
+
guard let flag = background as? NSNumber, CFGetTypeID(flag) == CFBooleanGetTypeID() else { return completion(failure("INVALID_ARGUMENT", "background must be boolean")) }
|
|
123
|
+
if flag.boolValue { return completion(failure("UNSUPPORTED", "Background pedometer streaming is not supported")) }
|
|
124
|
+
}
|
|
125
|
+
let current = now().timeIntervalSince1970 * 1000
|
|
126
|
+
let first: Double, last: Double
|
|
127
|
+
if method == "requestPermission" { first = current - 1000; last = current }
|
|
128
|
+
else {
|
|
129
|
+
guard let start = date(options["startDate"] ?? (method == "start" ? current : NSNull())),
|
|
130
|
+
let end = date(options["endDate"] ?? current), start >= current - 7 * 86400 * 1000,
|
|
131
|
+
start <= current, end <= current, (method == "start" ? start <= end : start < end) else {
|
|
132
|
+
return completion(failure("INVALID_ARGUMENT", "Dates must describe an interval within the last seven days"))
|
|
133
|
+
}
|
|
134
|
+
first = start; last = end
|
|
135
|
+
}
|
|
136
|
+
if method == "start" {
|
|
137
|
+
let previous = live
|
|
138
|
+
live = source
|
|
139
|
+
source.start(from: Date(timeIntervalSince1970: first / 1000)) { [weak self, weak source] result in
|
|
140
|
+
guard let self, let source, !self.closed, self.live === source else { return }
|
|
141
|
+
switch result {
|
|
142
|
+
case .success(let sample): self.emit(sample.payload)
|
|
143
|
+
case .failure: self.stop(reason: ["denied", "restricted"].contains(source.permission) ? "permissionDenied" : "sensorError")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
previous?.stop()
|
|
147
|
+
completion(.success(true))
|
|
148
|
+
} else {
|
|
149
|
+
pending[token] = source
|
|
150
|
+
source.query(from: Date(timeIntervalSince1970: first / 1000), to: Date(timeIntervalSince1970: last / 1000)) { [weak self, weak source] result in
|
|
151
|
+
guard let self, let source, !self.closed, self.pending[token] === source else { return }
|
|
152
|
+
self.pending.removeValue(forKey: token)
|
|
153
|
+
if method == "requestPermission", ["granted", "denied", "restricted"].contains(source.permission) {
|
|
154
|
+
completion(.success(source.permission)); return
|
|
155
|
+
}
|
|
156
|
+
switch result {
|
|
157
|
+
case .success(let sample): completion(.success(method == "requestPermission" ? source.permission as Any : sample.payload))
|
|
158
|
+
case .failure(let error): completion(self.failure(["denied", "restricted"].contains(source.permission) ? "PERMISSION_DENIED" : "SENSOR_ERROR", error.localizedDescription))
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private func date(_ value: Any) -> Double? {
|
|
165
|
+
guard let value = value as? NSNumber, CFGetTypeID(value) != CFBooleanGetTypeID(), value.doubleValue.isFinite else { return nil }
|
|
166
|
+
return value.doubleValue
|
|
167
|
+
}
|
|
168
|
+
func cancel(_ token: String) { pending.removeValue(forKey: token) }
|
|
169
|
+
@discardableResult func stop(reason: String) -> Bool {
|
|
170
|
+
guard let previous = live else { return false }
|
|
171
|
+
live = nil
|
|
172
|
+
previous.stop()
|
|
173
|
+
emit(["state": "stopped", "reason": reason])
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
func close() { closed = true; pending.removeAll(); stop(reason: "disposed"); emit = { _ in } }
|
|
177
|
+
deinit { live?.stop() }
|
|
178
|
+
private func failure(_ code: String, _ message: String) -> Result<Any, CraftNativeActionError> {
|
|
179
|
+
.failure(CraftNativeActionError(code: code, message: message))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -6191,6 +6191,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6191
6191
|
private var inputIdentities: [ObjectIdentifier: String] = [:]
|
|
6192
6192
|
private var inputDrafts: [String: String] = [:]
|
|
6193
6193
|
private var longPressHandlers: [ObjectIdentifier: String] = [:]
|
|
6194
|
+
private var accessibilityActionHandlers: [ObjectIdentifier: String] = [:]
|
|
6194
6195
|
private var textMaxLengths: [ObjectIdentifier: Int] = [:]
|
|
6195
6196
|
private var scrollHandlers: [ObjectIdentifier: String] = [:]
|
|
6196
6197
|
private var scrollBeginHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -6259,6 +6260,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6259
6260
|
private let audio = CraftWebAudio()
|
|
6260
6261
|
/// `craft.audio`'s sounds and the streaming responses this screen reads.
|
|
6261
6262
|
private let sounds = CraftNativeSounds()
|
|
6263
|
+
private let barometer = CraftNativeBarometer()
|
|
6264
|
+
private let pedometer = CraftNativePedometer()
|
|
6262
6265
|
private let streams = CraftNativeEventStreams()
|
|
6263
6266
|
/// A request a screen-owned module answers, by request token, and how to
|
|
6264
6267
|
/// call it off when it times out or is cancelled.
|
|
@@ -6595,6 +6598,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6595
6598
|
}
|
|
6596
6599
|
audio.close()
|
|
6597
6600
|
sounds.close()
|
|
6601
|
+
barometer.close()
|
|
6602
|
+
pedometer.close()
|
|
6598
6603
|
streams.close()
|
|
6599
6604
|
screenRequestCancellers.values.forEach { $0() }
|
|
6600
6605
|
screenRequestCancellers.removeAll()
|
|
@@ -8037,6 +8042,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8037
8042
|
guard let name = notification.userInfo?["event"] as? String else { return }
|
|
8038
8043
|
self?.send(type: "CRAFT_EVENT", payload: ["event": name, "detail": notification.userInfo?["detail"] ?? [:]])
|
|
8039
8044
|
})
|
|
8045
|
+
pedometer.emit = { [weak self] detail in self?.send(type: "CRAFT_EVENT", payload: ["event": "craftPedometerUpdate", "detail": detail]) }
|
|
8046
|
+
barometer.emit = { [weak self] detail in self?.send(type: "CRAFT_EVENT", payload: ["event": "craftBarometerUpdate", "detail": detail]) }
|
|
8040
8047
|
sounds.emit = { [weak self] payload in self?.send(type: "SOUND_EVENT", payload: payload) }
|
|
8041
8048
|
streams.emit = { [weak self] payload in self?.send(type: "NETWORK_STREAM", payload: payload) }
|
|
8042
8049
|
sendAppState(CraftNativeActions.currentAppState())
|
|
@@ -8201,6 +8208,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8201
8208
|
}
|
|
8202
8209
|
|
|
8203
8210
|
private func sendAppState(_ state: String) {
|
|
8211
|
+
if state != "active" { barometer.stop(reason: "background"); pedometer.stop(reason: "background") }
|
|
8204
8212
|
// A screen hears it while it is on top of its stack; the app script,
|
|
8205
8213
|
// which is on no stack, always.
|
|
8206
8214
|
guard isAppScript || navigationController?.topViewController === self else { return }
|
|
@@ -8412,6 +8420,29 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8412
8420
|
}
|
|
8413
8421
|
let id = args.first as? String ?? ""
|
|
8414
8422
|
switch (module, method) {
|
|
8423
|
+
case ("Pedometer", _):
|
|
8424
|
+
if !config.enablePedometer {
|
|
8425
|
+
reply(.failure(CraftNativeActionError(code: "CAPABILITY_DISABLED", message: "Pedometer is disabled")))
|
|
8426
|
+
} else if isAppScript {
|
|
8427
|
+
reply(.failure(CraftNativeActionError(code: "NEEDS_ACTIVITY", message: "Pedometer needs a visible app")))
|
|
8428
|
+
} else if ["start", "requestPermission"].contains(method) && UIApplication.shared.applicationState != .active {
|
|
8429
|
+
reply(.failure(CraftNativeActionError(code: "APP_INACTIVE", message: "Pedometer needs a foreground app")))
|
|
8430
|
+
} else {
|
|
8431
|
+
screenRequestCancellers[requestToken] = { [weak self] in self?.pedometer.cancel(requestToken) }
|
|
8432
|
+
pedometer.perform(method: method, args: args, token: requestToken, completion: reply)
|
|
8433
|
+
}
|
|
8434
|
+
return true
|
|
8435
|
+
case ("Barometer", _):
|
|
8436
|
+
if !config.enableBarometer {
|
|
8437
|
+
reply(.failure(CraftNativeActionError(code: "CAPABILITY_DISABLED", message: "Barometer is disabled")))
|
|
8438
|
+
} else if isAppScript {
|
|
8439
|
+
reply(.failure(CraftNativeActionError(code: "NEEDS_ACTIVITY", message: "Barometer needs a visible app")))
|
|
8440
|
+
} else if method == "start" && UIApplication.shared.applicationState != .active {
|
|
8441
|
+
reply(.failure(CraftNativeActionError(code: "APP_INACTIVE", message: "Barometer needs a foreground app")))
|
|
8442
|
+
} else {
|
|
8443
|
+
reply(barometer.perform(method: method, args: args))
|
|
8444
|
+
}
|
|
8445
|
+
return true
|
|
8415
8446
|
case ("AudioRecording", _):
|
|
8416
8447
|
guard config.enableAudioRecording else {
|
|
8417
8448
|
reply(.failure(CraftNativeActionError(code: "CAPABILITY_DISABLED", message: "Audio recording is disabled")))
|
|
@@ -9400,6 +9431,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9400
9431
|
}
|
|
9401
9432
|
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9402
9433
|
applyAccessibility(props, type: type, to: result)
|
|
9434
|
+
// After applyAccessibility: it settles whether the view is an element
|
|
9435
|
+
// at all, and an action on a non-element is unreachable.
|
|
9436
|
+
updateAccessibilityActions(props["accessibilityActions"], handler: props["disabled"] as? Bool == true ? nil : events["onAccessibilityAction"], for: result)
|
|
9403
9437
|
(result as? CraftNativeFlowView)?.configureCelebration(props["celebration"])
|
|
9404
9438
|
return current
|
|
9405
9439
|
}
|
|
@@ -9882,6 +9916,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9882
9916
|
inputEventCounts.removeValue(forKey: id)
|
|
9883
9917
|
if node.type == "TextInput" && !preservingInputDrafts { inputDrafts.removeValue(forKey: node.identity) }
|
|
9884
9918
|
longPressHandlers.removeValue(forKey: id)
|
|
9919
|
+
accessibilityActionHandlers.removeValue(forKey: id)
|
|
9885
9920
|
textMaxLengths.removeValue(forKey: id)
|
|
9886
9921
|
scrollHandlers.removeValue(forKey: id)
|
|
9887
9922
|
scrollBeginHandlers.removeValue(forKey: id)
|
|
@@ -10099,6 +10134,47 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10099
10134
|
}
|
|
10100
10135
|
}
|
|
10101
10136
|
|
|
10137
|
+
/// What a hold offers, published where the rotor can reach it.
|
|
10138
|
+
///
|
|
10139
|
+
/// A long press is invisible to VoiceOver: nothing announces that it is
|
|
10140
|
+
/// there and nothing can perform it, so an action living only behind a
|
|
10141
|
+
/// hold does not exist for anyone driving the screen by assistive
|
|
10142
|
+
/// technology. A thread that holds a message for Copy and Report is the
|
|
10143
|
+
/// case this was written for; without this the honest mitigation is a
|
|
10144
|
+
/// permanently visible button under every message.
|
|
10145
|
+
///
|
|
10146
|
+
/// Shape follows the rest of the accessibility props: `name` identifies
|
|
10147
|
+
/// the action to the handler, `label` is what a person hears, and the
|
|
10148
|
+
/// event says which one ran so one handler can serve them all.
|
|
10149
|
+
private func updateAccessibilityActions(_ raw: Any?, handler: String?, for view: UIView) {
|
|
10150
|
+
let id = ObjectIdentifier(view)
|
|
10151
|
+
let entries = (raw as? [[String: Any]] ?? []).compactMap { entry -> (name: String, label: String)? in
|
|
10152
|
+
guard let name = entry["name"] as? String, !name.isEmpty else { return nil }
|
|
10153
|
+
return (name, (entry["label"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? name)
|
|
10154
|
+
}
|
|
10155
|
+
guard let handler, !handler.isEmpty, !entries.isEmpty else {
|
|
10156
|
+
accessibilityActionHandlers.removeValue(forKey: id)
|
|
10157
|
+
view.accessibilityCustomActions = nil
|
|
10158
|
+
return
|
|
10159
|
+
}
|
|
10160
|
+
accessibilityActionHandlers[id] = handler
|
|
10161
|
+
view.accessibilityCustomActions = entries.map { entry in
|
|
10162
|
+
UIAccessibilityCustomAction(name: entry.label) { [weak self, weak view] _ in
|
|
10163
|
+
// Read the handler at firing time: a reconcile can rename it
|
|
10164
|
+
// while the rotor sits on the element.
|
|
10165
|
+
guard let self, let view, let current = self.accessibilityActionHandlers[ObjectIdentifier(view)] else { return false }
|
|
10166
|
+
self.send(type: "EVENT", payload: ["handlerName": current, "nativeEvent": ["actionName": entry.name]])
|
|
10167
|
+
return true
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
// The rotor only lands on an element, and actions on a view it walks
|
|
10171
|
+
// straight through are actions nobody can get to. A bubble whose own
|
|
10172
|
+
// label is its text is exactly that case.
|
|
10173
|
+
if !view.isAccessibilityElement && view.accessibilityElements == nil && view.accessibilityLabel != nil {
|
|
10174
|
+
view.isAccessibilityElement = true
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
|
|
10102
10178
|
/// Legacy runtimes echo values without a revision. Keep their bounded
|
|
10103
10179
|
/// history on either script thread; bridge delivery may still be delayed.
|
|
10104
10180
|
private var unechoedText: [ObjectIdentifier: [String]] = [:]
|
package/dist/macos/src/index.js
CHANGED
|
@@ -43,7 +43,7 @@ import process from "process";
|
|
|
43
43
|
var macosToolchain = {
|
|
44
44
|
zig: "0.18.0-dev.252+86dc5ce25",
|
|
45
45
|
sources: {
|
|
46
|
-
"zig-js": { repository: "zig-utils/zig-js", revision: "
|
|
46
|
+
"zig-js": { repository: "zig-utils/zig-js", revision: "95d835edcbad55a7456cff76a9bb7ef6cdf265fc", paths: ["src", "include", "tests", "tools", "docs", "bench", "conformance"] },
|
|
47
47
|
"zig-regex": { repository: "zig-utils/zig-regex", revision: "fdd9c2e333ea6ac2fd06d080d939ef7f11d61b1b", paths: ["src"] },
|
|
48
48
|
"zig-gc": { repository: "zig-utils/zig-gc", revision: "73c802e05768a729614e9c4296abb118aa06faf9", paths: ["src"] }
|
|
49
49
|
}
|
|
@@ -9,7 +9,7 @@ export declare const macosToolchain: {
|
|
|
9
9
|
readonly sources: {
|
|
10
10
|
readonly 'zig-js': {
|
|
11
11
|
readonly repository: 'zig-utils/zig-js';
|
|
12
|
-
readonly revision: '
|
|
12
|
+
readonly revision: '95d835edcbad55a7456cff76a9bb7ef6cdf265fc';
|
|
13
13
|
readonly paths: readonly ['src', 'include', 'tests', 'tools', 'docs', 'bench', 'conformance'];
|
|
14
14
|
};
|
|
15
15
|
readonly 'zig-regex': {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import AVKit
|
|
3
|
+
import ZigJS
|
|
3
4
|
import CoreImage
|
|
4
5
|
import UserNotifications
|
|
5
6
|
import UniformTypeIdentifiers
|
|
@@ -23,19 +24,31 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
|
|
|
23
24
|
var holdsQuit = false
|
|
24
25
|
/// The content width the screen last heard, so a resize that changes nothing sends nothing.
|
|
25
26
|
var reportedViewportWidth: CGFloat = -1
|
|
26
|
-
|
|
27
|
+
/// The content size the screen started with, which the window corrects once it shows.
|
|
28
|
+
let startedViewport: CGSize
|
|
29
|
+
private var shown = false
|
|
30
|
+
/// How long a window waits for its screen's first tree before it opens without it.
|
|
31
|
+
static let firstTreeGrace: TimeInterval = 0.5
|
|
32
|
+
/// `runtime` is the screen already started for this window (the main one starts before AppKit has
|
|
33
|
+
/// launched); without one the screen starts here, and evaluates its bundle while the window is made.
|
|
34
|
+
init(name: String, route: String, options: [String: Any], application: CraftMacApplication, runtime prepared: CraftMacRuntime? = nil) {
|
|
35
|
+
CraftTrace.mark("scene \(name)")
|
|
27
36
|
self.name = name
|
|
28
|
-
runtime = CraftMacRuntime(route: route, params: options["params"] as? [String: Any] ?? [:], network: application.network)
|
|
29
|
-
renderer = CraftMacRenderer(runtime: runtime)
|
|
30
37
|
let width = number(options["width"]) ?? 1120, height = number(options["height"]) ?? 740
|
|
31
38
|
let transparent = (options["titlebarStyle"] as? String ?? application.titlebarStyle) == "transparent"
|
|
39
|
+
let viewport = Self.contentSize(options: options, application: application, name: name)
|
|
40
|
+
runtime = prepared ?? application.startScreen(route: route, options: options, viewport: viewport, colorScheme: CraftMacColor.isDark ? "dark" : "light")
|
|
41
|
+
reportedViewportWidth = viewport.width
|
|
42
|
+
startedViewport = viewport
|
|
43
|
+
renderer = CraftMacRenderer(runtime: runtime)
|
|
32
44
|
let lights = options["trafficLights"] as? [Any] ?? (name == "main" ? application.trafficLights : nil)
|
|
33
45
|
trafficLights = lights.flatMap { values in values.count == 2 ? number(values[0]).flatMap { x in number(values[1]).map { CGPoint(x: x, y: $0) } } : nil }
|
|
34
|
-
let panel = CraftMacWindow(contentRect: CGRect(x: 0, y: 0, width: width, height: height), styleMask: transparent ? [.titled, .closable, .resizable, .miniaturizable, .fullSizeContentView] : [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer:
|
|
46
|
+
let panel = CraftMacWindow(contentRect: CGRect(x: 0, y: 0, width: width, height: height), styleMask: transparent ? [.titled, .closable, .resizable, .miniaturizable, .fullSizeContentView] : [.titled, .closable, .resizable, .miniaturizable], backing: .buffered, defer: true)
|
|
35
47
|
panel.runtime = runtime
|
|
36
48
|
window = panel
|
|
49
|
+
CraftTrace.mark("window created")
|
|
37
50
|
super.init()
|
|
38
|
-
|
|
51
|
+
renderer.window = window
|
|
39
52
|
window.title = options["title"] as? String ?? application.appName
|
|
40
53
|
if (options["titlebarStyle"] as? String ?? application.titlebarStyle) == "transparent" {
|
|
41
54
|
window.titlebarAppearsTransparent = true
|
|
@@ -48,14 +61,54 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
|
|
|
48
61
|
renderer.root.autoresizingMask = [.width, .height]
|
|
49
62
|
window.contentView = renderer.root
|
|
50
63
|
window.delegate = self; window.center()
|
|
51
|
-
if
|
|
52
|
-
window.
|
|
64
|
+
if let autosave = Self.autosaveName(name: name, application: application) { window.setFrameAutosaveName(autosave) }
|
|
65
|
+
// Whatever the screen drew while the window was made is in it before it shows. A screen that has not
|
|
66
|
+
// drawn yet gets a moment to, so the window opens with its content rather than blank.
|
|
67
|
+
runtime.attach(self)
|
|
68
|
+
if runtime.hasDrawn { show() } else {
|
|
69
|
+
runtime.whenDrawn { [weak self] in self?.show() }
|
|
70
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.firstTreeGrace) { [weak self] in self?.show() }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Orders the window front the first time. The window is made deferred, so the window server's side
|
|
75
|
+
/// of it is made here, once, rather than updated for each property set while it was configured; and
|
|
76
|
+
/// it opens without the zoom AppKit plays for a new window, which runs a display link and draws it in
|
|
77
|
+
/// over several frames. Later showings animate as usual.
|
|
78
|
+
func show() {
|
|
79
|
+
guard !shown else { return }
|
|
80
|
+
shown = true
|
|
81
|
+
let behavior = window.animationBehavior
|
|
82
|
+
window.animationBehavior = .none
|
|
83
|
+
CraftTrace.span("window ordered front") { window.makeKeyAndOrderFront(nil) }
|
|
84
|
+
window.animationBehavior = behavior
|
|
53
85
|
placeTrafficLights()
|
|
54
|
-
// The screen
|
|
86
|
+
// The screen started with the size this window was expected to open at; a saved frame the
|
|
87
|
+
// screen cannot fit, or a smaller display, makes it another, which the screen now hears.
|
|
55
88
|
let size = window.contentLayoutRect.size
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
if abs(size.width - startedViewport.width) >= 0.5 || abs(size.height - startedViewport.height) >= 0.5 {
|
|
90
|
+
reportedViewportWidth = size.width
|
|
91
|
+
runtime.send("VIEWPORT", ["width": Double(size.width), "height": Double(size.height)])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static func autosaveName(name: String, application: CraftMacApplication) -> String? {
|
|
96
|
+
name == "main" && application.testDirectory == nil ? "\(application.appName)NativeMain" : nil
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// The content size (below the title bar) the window will open at, known before it exists: the size
|
|
100
|
+
/// asked for, or the frame AppKit saved for it last time. Its screen starts with it.
|
|
101
|
+
static func contentSize(options: [String: Any], application: CraftMacApplication, name: String) -> CGSize {
|
|
102
|
+
let transparent = (options["titlebarStyle"] as? String ?? application.titlebarStyle) == "transparent"
|
|
103
|
+
let titlebar = NSWindow.frameRect(forContentRect: CGRect(x: 0, y: 0, width: 100, height: 100), styleMask: [.titled]).height - 100
|
|
104
|
+
let width = number(options["width"]) ?? 1120, height = number(options["height"]) ?? 740
|
|
105
|
+
// `x y width height` of the frame, then the screen's.
|
|
106
|
+
if let autosave = autosaveName(name: name, application: application),
|
|
107
|
+
let saved = UserDefaults.standard.string(forKey: "NSWindow Frame \(autosave)")?.split(separator: " ").compactMap({ Double($0) }), saved.count >= 4 {
|
|
108
|
+
return CGSize(width: saved[2], height: max(0, saved[3] - titlebar))
|
|
109
|
+
}
|
|
110
|
+
// A full-size content view runs under the title bar, which its layout rect leaves out.
|
|
111
|
+
return CGSize(width: width, height: transparent ? max(0, height - titlebar) : height)
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
/// Tells the screen its new width (VIEWPORT, as a phone does on rotation): only when the width changed,
|
|
@@ -109,8 +162,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
109
162
|
var titlebarStyle = "standard"
|
|
110
163
|
var trafficLights: [Any]?
|
|
111
164
|
/// Apps that ship their own agent (Attic) hand out a session cookie at
|
|
112
|
-
/// `/native/session
|
|
113
|
-
/// endpoint and starts with `--no-session`.
|
|
165
|
+
/// `/native/session` (`authorize`). An app that talks to a hosted API has
|
|
166
|
+
/// no such endpoint and starts with `--no-session`.
|
|
114
167
|
var requiresSession = true
|
|
115
168
|
var mainWindowOptions: [String: Any] = [:]
|
|
116
169
|
/// The Dock icon's menu (`craft-app.json` `dockMenu`): each item sends its action as a desktop command.
|
|
@@ -128,34 +181,46 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
128
181
|
var testMenu: [[String: Any]] = []
|
|
129
182
|
var testMenuChoice: String?
|
|
130
183
|
var appearanceObservation: NSKeyValueObservation?
|
|
184
|
+
/// The main window's screen, started before AppKit launched, waiting for its window.
|
|
185
|
+
var preparedMain: CraftMacRuntime?
|
|
186
|
+
/// The appearance it started with, which AppKit could not be asked yet.
|
|
187
|
+
var preparedColorScheme = ""
|
|
188
|
+
var mainOptions: [String: Any] { mainWindowOptions.merging(["title": appName]) { _, new in new } }
|
|
189
|
+
/// Launch finished, and why the agent issued no session, if it did not (shown once both are known).
|
|
190
|
+
var launched = false
|
|
191
|
+
var sessionFailure: String?
|
|
131
192
|
/// A quit waiting for screens to finish (`craft.desktop.onQuit`), for at most `quitGrace` seconds.
|
|
132
193
|
private var pendingQuit: (token: String, waiting: Set<String>, timeout: Timer)?
|
|
133
194
|
static let quitGrace: TimeInterval = 3
|
|
134
195
|
|
|
196
|
+
func applicationWillFinishLaunching(_ notification: Notification) { CraftTrace.mark("will finish launching") }
|
|
135
197
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
136
|
-
|
|
198
|
+
CraftTrace.mark("did finish launching")
|
|
199
|
+
CraftTrace.span("menus installed") { installMenus() }
|
|
137
200
|
appearanceObservation = NSApp.observe(\.effectiveAppearance, options: [.new]) { [weak self] _, _ in
|
|
138
201
|
DispatchQueue.main.async { self?.appearanceChanged() }
|
|
139
202
|
}
|
|
140
203
|
NSApp.activate(ignoringOtherApps: true)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
204
|
+
open(name: "main", route: initialRoute, options: mainOptions)
|
|
205
|
+
if preparedColorScheme != (CraftMacColor.isDark ? "dark" : "light") { appearanceChanged() }
|
|
206
|
+
startTestDriver()
|
|
207
|
+
launched = true
|
|
208
|
+
if let failure = sessionFailure { sessionFailed(failure) }
|
|
209
|
+
}
|
|
210
|
+
/// Asks the agent for the session cookie (`CraftMacNetwork.authorize`) as the app starts, before
|
|
211
|
+
/// AppKit has launched; the screen's requests wait for it. Without one the app says so and quits.
|
|
212
|
+
func authorize() {
|
|
213
|
+
network.authorize { failure in
|
|
150
214
|
DispatchQueue.main.async {
|
|
151
|
-
|
|
152
|
-
guard
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
self.open(name: "main", route: self.initialRoute, options: self.mainWindowOptions.merging(["title": self.appName]) { _, new in new })
|
|
156
|
-
self.startTestDriver()
|
|
215
|
+
CraftTrace.mark("native session answered")
|
|
216
|
+
guard let failure else { return }
|
|
217
|
+
self.sessionFailure = failure
|
|
218
|
+
if self.launched { self.sessionFailed(failure) }
|
|
157
219
|
}
|
|
158
|
-
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
private func sessionFailed(_ message: String) {
|
|
223
|
+
let alert = NSAlert(); alert.messageText = "Could not connect to the app"; alert.informativeText = message; alert.runModal(); NSApp.terminate(nil)
|
|
159
224
|
}
|
|
160
225
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { scenes["main"]?.window.makeKeyAndOrderFront(nil); return true }
|
|
161
226
|
/// Links from other apps (this app as the default browser) and files dropped on the Dock icon.
|
|
@@ -212,7 +277,24 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
212
277
|
}
|
|
213
278
|
func open(name: String, route: String, options: [String: Any]) {
|
|
214
279
|
if let scene = scenes[name] { scene.window.makeKeyAndOrderFront(nil); return }
|
|
215
|
-
|
|
280
|
+
let prepared = name == "main" ? preparedMain : nil
|
|
281
|
+
if prepared != nil { preparedMain = nil }
|
|
282
|
+
scenes[name] = CraftMacScene(name: name, route: route, options: options, application: self, runtime: prepared)
|
|
283
|
+
}
|
|
284
|
+
/// A screen's thread and context, its bundle evaluated as soon as the thread can.
|
|
285
|
+
func startScreen(route: String, options: [String: Any], viewport: CGSize, colorScheme: String) -> CraftMacRuntime {
|
|
286
|
+
let runtime = CraftMacRuntime(route: route, params: options["params"] as? [String: Any] ?? [:], network: network, colorScheme: colorScheme)
|
|
287
|
+
runtime.load(bundle, viewport: viewport)
|
|
288
|
+
return runtime
|
|
289
|
+
}
|
|
290
|
+
/// Starts the main window's screen before AppKit launches: connecting to the window server and
|
|
291
|
+
/// launching take a few hundred milliseconds, most of them waiting, and the screen evaluates its
|
|
292
|
+
/// bundle, mounts and sends its first requests meanwhile. Its tree is drawn when the window opens.
|
|
293
|
+
func prepareMainScreen() {
|
|
294
|
+
// NSApp does not exist yet, so the system's setting stands in for AppKit's appearance;
|
|
295
|
+
// launching checks it against AppKit's.
|
|
296
|
+
preparedColorScheme = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" ? "dark" : "light"
|
|
297
|
+
preparedMain = startScreen(route: initialRoute, options: mainOptions, viewport: CraftMacScene.contentSize(options: mainOptions, application: self, name: "main"), colorScheme: preparedColorScheme)
|
|
216
298
|
}
|
|
217
299
|
/// A menu command goes to the window it was chosen in (the key window), else the main one: with
|
|
218
300
|
/// two windows of the same screen open, cmd-T opens one tab, not one in each.
|
|
@@ -231,7 +313,7 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
231
313
|
// AppKit lays the title bar out again for the new appearance, after this returns.
|
|
232
314
|
scene.placeTrafficLights()
|
|
233
315
|
DispatchQueue.main.async { [weak scene] in scene?.placeTrafficLights() }
|
|
234
|
-
scene.runtime.
|
|
316
|
+
scene.runtime.evaluate("__stxNativeBridge.colorScheme = '\(scheme)';")
|
|
235
317
|
// The protocol's own message: the screen restyles its `dark:` classes and tells `craft.appearance.onChange`.
|
|
236
318
|
scene.runtime.send("APPEARANCE", ["colorScheme": scheme])
|
|
237
319
|
scene.window.effectiveAppearance.performAsCurrentDrawingAppearance {
|
|
@@ -293,7 +375,7 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
293
375
|
if parts.contains("alt") { item.keyEquivalentModifierMask.insert(.option) }
|
|
294
376
|
item.isEnabled = raw["disabled"] as? Bool != true
|
|
295
377
|
item.state = raw["checked"] as? Bool == true ? .on : .off
|
|
296
|
-
if let symbol = raw["icon"] as? String { item.image =
|
|
378
|
+
if let symbol = raw["icon"] as? String { item.image = CraftMacSymbols.image(symbol) }
|
|
297
379
|
return item
|
|
298
380
|
}
|
|
299
381
|
func perform(_ method: String, args: [Any], scene: CraftMacScene?, reply: @escaping (Any) -> Void) {
|
|
@@ -373,10 +455,10 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
373
455
|
guard let path = file["path"], chosenFiles.contains(path), let data = try? Data(contentsOf: URL(fileURLWithPath: path)), data.count <= 256 * 1024 * 1024,
|
|
374
456
|
var request = network.request(["url": endpoint, "method": "POST", "headers": ["x-filename": file["name"] ?? "Attachment", "content-type": file["type"] ?? "application/octet-stream"]]) else { reply(["error": "Could not read a picked attachment"]); return }
|
|
375
457
|
request.httpBody = data
|
|
376
|
-
network.session.dataTask(with: request) { data, response, error in DispatchQueue.main.async {
|
|
458
|
+
network.resume(network.session.dataTask(with: request) { data, response, error in DispatchQueue.main.async {
|
|
377
459
|
guard error == nil, (response as? HTTPURLResponse)?.statusCode == 200, let data, let value = try? JSONSerialization.jsonObject(with: data) else { reply(["error": error?.localizedDescription ?? "Attachment upload failed"]); return }
|
|
378
460
|
results.append(value); next(index + 1)
|
|
379
|
-
} }
|
|
461
|
+
} })
|
|
380
462
|
}; next(0)
|
|
381
463
|
}
|
|
382
464
|
private func preview(_ options: [String: Any], scene: CraftMacScene?) {
|
|
@@ -390,18 +472,18 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
390
472
|
player.player?.play()
|
|
391
473
|
} else {
|
|
392
474
|
let image = NSImageView(); image.imageScaling = .scaleProportionallyUpOrDown; panel.contentView = image
|
|
393
|
-
network.session.dataTask(with: request) { data, _, _ in DispatchQueue.main.async { image.image = data.flatMap(NSImage.init(data:)) } }
|
|
475
|
+
network.resume(network.session.dataTask(with: request) { data, _, _ in DispatchQueue.main.async { image.image = data.flatMap(NSImage.init(data:)) } })
|
|
394
476
|
}
|
|
395
477
|
panel.center(); panel.makeKeyAndOrderFront(nil)
|
|
396
478
|
}
|
|
397
479
|
private func export(_ options: [String: Any], reply: @escaping (Any) -> Void) {
|
|
398
480
|
let panel = NSSavePanel(); panel.nameFieldStringValue = options["name"] as? String ?? "Attachment"
|
|
399
481
|
guard panel.runModal() == .OK, let destination = panel.url, let request = network.request(["url": options["url"] ?? ""]) else { reply(false); return }
|
|
400
|
-
network.session.downloadTask(with: request) { location, response, error in
|
|
482
|
+
network.resume(network.session.downloadTask(with: request) { location, response, error in
|
|
401
483
|
var saved = false
|
|
402
484
|
if error == nil, (response as? HTTPURLResponse)?.statusCode == 200, let location { do { if FileManager.default.fileExists(atPath: destination.path) { try FileManager.default.removeItem(at: destination) }; try FileManager.default.moveItem(at: location, to: destination); saved = true } catch {} }
|
|
403
485
|
DispatchQueue.main.async { reply(saved) }
|
|
404
|
-
}
|
|
486
|
+
})
|
|
405
487
|
}
|
|
406
488
|
func notification(_ method: String, options: [String: Any], reply: @escaping (Any) -> Void) {
|
|
407
489
|
guard method == "schedule" else { reply(false); return }
|
|
@@ -554,7 +636,7 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
554
636
|
return node
|
|
555
637
|
}
|
|
556
638
|
func classes(_ view: NSView) -> [String] { [String(describing: type(of: view))] + view.subviews.flatMap(classes) }
|
|
557
|
-
return ["name": scene.name, "title": scene.window.title, "route": scene.runtime.route, "contextClass": String(describing:
|
|
639
|
+
return ["name": scene.name, "title": scene.window.title, "route": scene.runtime.route, "contextClass": String(describing: ZigJSContext.self), "nodes": nodes, "viewClasses": classes(scene.renderer.root), "viewportWidth": Double(scene.reportedViewportWidth), "errors": scene.runtime.errors]
|
|
558
640
|
}
|
|
559
641
|
let webViews = windows.reduce(0) { count, window in count + ((window["viewClasses"] as? [String] ?? []).filter { $0.contains("WKWebView") || $0 == "WebView" }.count) }
|
|
560
642
|
return ["renderer": "appkit", "scriptEngine": "zig-js", "appearance": CraftMacColor.isDark ? "dark" : "light", "windows": windows, "webViews": webViews, "menu": testMenu]
|
|
@@ -563,6 +645,7 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
563
645
|
|
|
564
646
|
@main struct CraftMacMain {
|
|
565
647
|
static func main() throws {
|
|
648
|
+
CraftTrace.mark("main")
|
|
566
649
|
// A bundled app is launched by Finder with no arguments: its settings are
|
|
567
650
|
// `Contents/Resources/craft-app.json`, written by the build. A flag on the
|
|
568
651
|
// command line still wins, so a development run can point elsewhere.
|
|
@@ -595,6 +678,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
|
|
|
595
678
|
application.network = CraftMacNetwork(base: api)
|
|
596
679
|
let testDirectory = argument("--test-dir")
|
|
597
680
|
if !testDirectory.isEmpty { application.testDirectory = URL(fileURLWithPath: testDirectory, isDirectory: true) }
|
|
681
|
+
if application.requiresSession { application.authorize() }
|
|
682
|
+
application.prepareMainScreen()
|
|
598
683
|
let app = NSApplication.shared
|
|
599
684
|
app.setActivationPolicy(.regular); app.delegate = application; app.run()
|
|
600
685
|
}
|