craft-native 0.0.497 → 0.0.500
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/CraftAppLinks.kt.template +28 -0
- package/dist/android/templates/CraftBridge.kt.template +3 -29
- package/dist/android/templates/CraftNativeBackground.kt.template +21 -2
- package/dist/android/templates/CraftNativeCapabilities.kt.template +145 -28
- package/dist/android/templates/CraftNativeFlatList.kt.template +51 -3
- package/dist/android/templates/CraftNativeShell.kt.template +102 -2
- package/dist/android/templates/CraftNativeSounds.kt.template +22 -3
- package/dist/android/templates/MainActivityNative.kt.template +44 -14
- package/dist/api/process.d.ts +23 -28
- package/dist/bridge/core.d.ts +30 -6
- package/dist/browser.js +27 -5
- package/dist/cli.js +39 -19
- package/dist/components/index.d.ts +1 -1
- package/dist/index.cjs +90 -99
- package/dist/index.js +90 -99
- package/dist/ios/templates/CraftApp.swift +25 -1
- package/dist/ios/templates/CraftHybrid.swift +63 -22
- package/dist/ios/templates/CraftNativeActions.swift +47 -3
- package/dist/ios/templates/CraftNativeFiles.swift +160 -8
- package/dist/ios/templates/CraftNativeFlatList.swift +43 -6
- package/dist/ios/templates/CraftNativeScreen.swift +222 -69
- package/dist/ios/templates/CraftPersistentStorage.swift +28 -5
- package/dist/ios/templates/CraftScheduledWorkouts.swift +11 -4
- package/dist/ios/templates/CraftWebAudio.swift +20 -1
- package/dist/package.d.ts +22 -0
- package/package.json +1 -1
|
@@ -13,11 +13,22 @@ enum CraftPersistentStorage {
|
|
|
13
13
|
let digest = SHA256.hash(data: Data(key.utf8)).map { String(format: "%02x", $0) }.joined()
|
|
14
14
|
return directory.appendingPathComponent(digest + ".json")
|
|
15
15
|
}
|
|
16
|
+
/// What an entry file holds, or nil when it is there and is not an entry.
|
|
17
|
+
///
|
|
18
|
+
/// A file that cannot be parsed is a corrupt value, as one that parses to
|
|
19
|
+
/// the wrong thing is, and an app starts that value again. Only a file
|
|
20
|
+
/// that cannot be read at all is the disk failing, which an app retries;
|
|
21
|
+
/// that still throws. The two used to share the disk's answer.
|
|
22
|
+
private static func entry(at source: URL) throws -> [String: Any]? {
|
|
23
|
+
let contents = try Data(contentsOf: source)
|
|
24
|
+
return (try? JSONSerialization.jsonObject(with: contents)) as? [String: Any]
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
static func get(_ key: String, prefix: String, storageDirectory: URL? = nil) throws -> Data? {
|
|
17
28
|
lock.lock(); defer { lock.unlock() }
|
|
18
29
|
let source = path(key, directory: storageDirectory ?? directory)
|
|
19
30
|
if FileManager.default.fileExists(atPath: source.path) {
|
|
20
|
-
guard let saved = try
|
|
31
|
+
guard let saved = try entry(at: source), saved["key"] as? String == key else {
|
|
21
32
|
throw CraftNativeActionError(code: "STORAGE_CORRUPT", message: "Invalid stored value")
|
|
22
33
|
}
|
|
23
34
|
if saved["deleted"] as? Bool == true { return nil }
|
|
@@ -26,7 +37,9 @@ enum CraftPersistentStorage {
|
|
|
26
37
|
}
|
|
27
38
|
guard let legacy = UserDefaults.standard.data(forKey: prefix + key) else { return nil }
|
|
28
39
|
// Only valid legacy JSON qualifies for deferred migration on I/O failure.
|
|
29
|
-
|
|
40
|
+
guard (try? JSONSerialization.jsonObject(with: legacy, options: .fragmentsAllowed)) != nil else {
|
|
41
|
+
throw CraftNativeActionError(code: "STORAGE_CORRUPT", message: "Stored value is not valid JSON")
|
|
42
|
+
}
|
|
30
43
|
// Reading a valid legacy checkpoint must survive a full disk. Migration
|
|
31
44
|
// can be retried on a later read; only a successful set promises a new
|
|
32
45
|
// durable checkpoint and permits the application to acknowledge it.
|
|
@@ -84,9 +97,11 @@ enum CraftPersistentStorage {
|
|
|
84
97
|
var result = Set(UserDefaults.standard.dictionaryRepresentation().keys.filter { $0.hasPrefix(prefix) }.map { String($0.dropFirst(prefix.count)) })
|
|
85
98
|
if FileManager.default.fileExists(atPath: directory.path) {
|
|
86
99
|
for source in try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) where source.pathExtension == "json" {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
// A file that is not an entry names no key. It used to fail
|
|
101
|
+
// the whole listing, and `clear` with it, over a value
|
|
102
|
+
// neither call had been asked about. `get` of its key still
|
|
103
|
+
// answers STORAGE_CORRUPT.
|
|
104
|
+
guard let entry = try entry(at: source), let key = entry["key"] as? String else { continue }
|
|
90
105
|
if entry["deleted"] as? Bool == true { result.remove(key) } else { result.insert(key) }
|
|
91
106
|
}
|
|
92
107
|
}
|
|
@@ -95,5 +110,13 @@ enum CraftPersistentStorage {
|
|
|
95
110
|
static func clear(prefix: String) throws {
|
|
96
111
|
lock.lock(); defer { lock.unlock() }
|
|
97
112
|
for key in try keys(prefix: prefix) { try set(key, data: nil, prefix: prefix) }
|
|
113
|
+
// Clearing a key writes over its file, so what is still not an entry
|
|
114
|
+
// belongs to no key this store knows, and cannot be marked removed.
|
|
115
|
+
// It is deleted. No earlier value can come back in its place: every
|
|
116
|
+
// one this store mirrors was removed just above.
|
|
117
|
+
guard FileManager.default.fileExists(atPath: directory.path) else { return }
|
|
118
|
+
for source in try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) where source.pathExtension == "json" {
|
|
119
|
+
if try entry(at: source)?["key"] as? String == nil { try FileManager.default.removeItem(at: source) }
|
|
120
|
+
}
|
|
98
121
|
}
|
|
99
122
|
}
|
|
@@ -317,6 +317,13 @@ enum CraftWorkoutPlanBuilder {
|
|
|
317
317
|
return double.isFinite ? double : nil
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/// A number as a whole one, toward zero, or nil when no `Int` holds it.
|
|
321
|
+
/// `Int(_:)` traps there, and these come from the caller: an `hour` of
|
|
322
|
+
/// `1e30` ended the process where it should have been refused (#510).
|
|
323
|
+
private static func whole(_ value: Any?) -> Int? {
|
|
324
|
+
number(value).flatMap { Int(exactly: $0.rounded(.towardZero)) }
|
|
325
|
+
}
|
|
326
|
+
|
|
320
327
|
/// Two schedule dates as WorkoutKit compares them: the day and the time of day.
|
|
321
328
|
static func sameMoment(_ a: DateComponents, _ b: DateComponents) -> Bool {
|
|
322
329
|
a.year == b.year && a.month == b.month && a.day == b.day && (a.hour ?? 0) == (b.hour ?? 0) && (a.minute ?? 0) == (b.minute ?? 0)
|
|
@@ -328,8 +335,8 @@ enum CraftWorkoutPlanBuilder {
|
|
|
328
335
|
let parts = text.split(separator: "-", omittingEmptySubsequences: false)
|
|
329
336
|
guard parts.count == 3, parts[0].count == 4, parts[1].count == 2, parts[2].count == 2,
|
|
330
337
|
let year = Int(parts[0]), let month = Int(parts[1]), let day = Int(parts[2]) else { return nil }
|
|
331
|
-
let hour = body["hour"] == nil || body["hour"] is NSNull ? 7 :
|
|
332
|
-
let minute = body["minute"] == nil || body["minute"] is NSNull ? 0 :
|
|
338
|
+
let hour = body["hour"] == nil || body["hour"] is NSNull ? 7 : whole(body["hour"])
|
|
339
|
+
let minute = body["minute"] == nil || body["minute"] is NSNull ? 0 : whole(body["minute"])
|
|
333
340
|
guard let hour, let minute, (0...23).contains(hour), (0...59).contains(minute) else { return nil }
|
|
334
341
|
var components = DateComponents()
|
|
335
342
|
components.year = year
|
|
@@ -379,7 +386,7 @@ enum CraftWorkoutPlanBuilder {
|
|
|
379
386
|
}
|
|
380
387
|
func zone() -> Int? {
|
|
381
388
|
guard let value = number(body["zone"]), value >= 1, value.rounded() == value else { return nil }
|
|
382
|
-
return Int(value)
|
|
389
|
+
return Int(exactly: value)
|
|
383
390
|
}
|
|
384
391
|
switch type {
|
|
385
392
|
case "heartRate":
|
|
@@ -485,7 +492,7 @@ enum CraftWorkoutPlanBuilder {
|
|
|
485
492
|
var blocks: [IntervalBlock] = []
|
|
486
493
|
for (blockIndex, value) in (workout["blocks"] as? [Any] ?? []).enumerated() {
|
|
487
494
|
let block = value as? [String: Any] ?? [:]
|
|
488
|
-
let iterations =
|
|
495
|
+
let iterations = whole(block["iterations"] ?? 1) ?? 0
|
|
489
496
|
guard iterations >= 1 else { return refuse("block \(blockIndex + 1) needs iterations of 1 or more") }
|
|
490
497
|
var steps: [IntervalStep] = []
|
|
491
498
|
for (stepIndex, entry) in (block["steps"] as? [Any] ?? []).enumerated() {
|
|
@@ -503,6 +503,25 @@ final class CraftAudioBufferSource: CraftAudioSource {
|
|
|
503
503
|
private var played = 0.0
|
|
504
504
|
private var began = false
|
|
505
505
|
|
|
506
|
+
/// An arbitrary limit on the rate, and WebKit's: `MaxRate` in its
|
|
507
|
+
/// AudioBufferSourceNode.
|
|
508
|
+
static let maximumRate = 1024.0
|
|
509
|
+
|
|
510
|
+
/// The rate a buffer plays at: its own sample rate over the context's,
|
|
511
|
+
/// times `playbackRate`, times two to the power of `detune` in octaves.
|
|
512
|
+
///
|
|
513
|
+
/// Neither param is bounded for a buffer source, so the product can be
|
|
514
|
+
/// infinite (a detune of 2,000,000 cents) or not a number (that, with a
|
|
515
|
+
/// playbackRate of 0). A looping source wrapped an infinite position into
|
|
516
|
+
/// one that is not a number, and `Int(position)` traps on it: the render
|
|
517
|
+
/// thread ended the process. WebKit clamps the rate to its limit either
|
|
518
|
+
/// side of zero and plays a rate that is not a number at 1; so does this.
|
|
519
|
+
static func rate(ratio: Double, playbackRate: Double, detune: Double) -> Double {
|
|
520
|
+
let rate = ratio * playbackRate * pow(2, detune / 1200)
|
|
521
|
+
guard !rate.isNaN else { return 1 }
|
|
522
|
+
return max(-maximumRate, min(maximumRate, rate))
|
|
523
|
+
}
|
|
524
|
+
|
|
506
525
|
override init(id: Int) {
|
|
507
526
|
super.init(id: id)
|
|
508
527
|
params = [playbackRate, detune]
|
|
@@ -530,7 +549,7 @@ final class CraftAudioBufferSource: CraftAudioSource {
|
|
|
530
549
|
position = loopFrom + (position - loopFrom).truncatingRemainder(dividingBy: span)
|
|
531
550
|
}
|
|
532
551
|
}
|
|
533
|
-
let rate = buffer.sampleRate / render.sampleRate
|
|
552
|
+
let rate = Self.rate(ratio: buffer.sampleRate / render.sampleRate, playbackRate: Double(playbackRate.values[0]), detune: Double(detune.values[0]))
|
|
534
553
|
let first = buffer.channels[0]
|
|
535
554
|
let second = buffer.channels.count > 1 ? buffer.channels[1] : first
|
|
536
555
|
let frames = buffer.length
|
package/dist/package.d.ts
CHANGED
|
@@ -376,6 +376,28 @@ export declare function windowsExecutableName(name: string): string;
|
|
|
376
376
|
* off, so nothing ever reported it.
|
|
377
377
|
*/
|
|
378
378
|
export declare function candleArguments(architecture: 'x86' | 'x64' | 'arm64', wixobjPath: string, wxsPath: string): string[];
|
|
379
|
+
/**
|
|
380
|
+
* Helper: Create Linux RPM package
|
|
381
|
+
*/
|
|
382
|
+
/**
|
|
383
|
+
* The spec an app's rpm is built from: one prebuilt executable, installed as
|
|
384
|
+
* it is.
|
|
385
|
+
*
|
|
386
|
+
* `Source0` names the executable, which is what `%{SOURCE0}` in `%install`
|
|
387
|
+
* refers to; without the tag the macro is not defined and the install step
|
|
388
|
+
* fails. The executable is a finished program, so nothing is stripped from it,
|
|
389
|
+
* no debug package is split off and no build-id links are made: a compiled
|
|
390
|
+
* app that carries its own data after the program (a Bun or Deno executable)
|
|
391
|
+
* stops working when strip rewrites it. With nothing required, the `Requires`
|
|
392
|
+
* line is left out, because an empty one is not valid.
|
|
393
|
+
*/
|
|
394
|
+
export declare function rpmSpec(options: {
|
|
395
|
+
name: string;
|
|
396
|
+
version: string;
|
|
397
|
+
summary: string;
|
|
398
|
+
description: string;
|
|
399
|
+
requires?: string;
|
|
400
|
+
}): string;
|
|
379
401
|
/**
|
|
380
402
|
* Simple packaging function for quick use
|
|
381
403
|
*/
|