@swiftbrowser/react 0.18.3 → 0.21.0
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/README.md +67 -11
- package/dist/audioEngine.d.ts +359 -0
- package/dist/audioEngine.d.ts.map +1 -0
- package/dist/components/containers.d.ts +4 -1
- package/dist/components/containers.d.ts.map +1 -1
- package/dist/components/controls.d.ts +2 -0
- package/dist/components/controls.d.ts.map +1 -1
- package/dist/components/lists.d.ts +3 -0
- package/dist/components/lists.d.ts.map +1 -1
- package/dist/components/navigation.d.ts.map +1 -1
- package/dist/components/registered.d.ts +30 -0
- package/dist/components/registered.d.ts.map +1 -0
- package/dist/components/split.d.ts +1 -1
- package/dist/components/split.d.ts.map +1 -1
- package/dist/components/stacks.d.ts +6 -2
- package/dist/components/stacks.d.ts.map +1 -1
- package/dist/components/system.d.ts +14 -4
- package/dist/components/system.d.ts.map +1 -1
- package/dist/dragDrop.d.ts.map +1 -1
- package/dist/fileImporter.d.ts +6 -1
- package/dist/fileImporter.d.ts.map +1 -1
- package/dist/fileManager.d.ts +22 -4
- package/dist/fileManager.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1045 -80
- package/dist/index.js.map +1 -1
- package/dist/languageModel.d.ts +136 -0
- package/dist/languageModel.d.ts.map +1 -0
- package/dist/modifiers.d.ts +19 -0
- package/dist/modifiers.d.ts.map +1 -1
- package/dist/mount.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,7 +14,9 @@ import { fileURL } from "@swiftbrowser/web/src/files";
|
|
|
14
14
|
import { registerHostContent, notifyHostContentChange, unregisterHostContent } from "@swiftbrowser/web/src/hostContent";
|
|
15
15
|
import { createRoot } from "react-dom/client";
|
|
16
16
|
import { flushSync } from "react-dom";
|
|
17
|
+
import { ViewRegistry } from "@swiftbrowser/web/src/viewRegistry";
|
|
17
18
|
import { chooseColor } from "@swiftbrowser/web/src/colorPicker";
|
|
19
|
+
import { base64Encode } from "@swiftbrowser/web/src/base64";
|
|
18
20
|
import { timingFor } from "@swiftbrowser/web/src/animation";
|
|
19
21
|
const ViewEnvironmentContext = createContext({});
|
|
20
22
|
function useViewEnvironment() {
|
|
@@ -646,6 +648,7 @@ function mount(screen, element, options = {}) {
|
|
|
646
648
|
let renderer = null;
|
|
647
649
|
const root = createSwiftUIRoot((ops) => renderer?.applyOps(ops));
|
|
648
650
|
renderer = new Renderer(screen, (event) => root.dispatch(event));
|
|
651
|
+
renderer.fileDrops = "reference";
|
|
649
652
|
const measurer = (text, font2, width) => renderer.measureText(text, font2, width);
|
|
650
653
|
installTextMeasurer(measurer);
|
|
651
654
|
let pageScheme = options.colorScheme ?? "light";
|
|
@@ -1071,10 +1074,21 @@ function itemValue(item) {
|
|
|
1071
1074
|
}
|
|
1072
1075
|
return item.text;
|
|
1073
1076
|
}
|
|
1077
|
+
const name = item.path ?? item.name ?? "file";
|
|
1078
|
+
if (item.file) {
|
|
1079
|
+
const file = item.file;
|
|
1080
|
+
return item.path !== void 0 && item.path !== file.name ? new File([file], item.path, { type: file.type, lastModified: file.lastModified }) : file;
|
|
1081
|
+
}
|
|
1082
|
+
if (item.url !== void 0 && item.data === void 0) {
|
|
1083
|
+
const fromReference = browserEnvironment.fileFromReference;
|
|
1084
|
+
if (fromReference) {
|
|
1085
|
+
return fromReference({ type: item.type, name, url: item.url, ...item.path !== void 0 ? { path: item.path } : {}, ...item.size !== void 0 ? { size: item.size } : {} });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1074
1088
|
const binary = atob(item.data ?? "");
|
|
1075
1089
|
const bytes = new Uint8Array(binary.length);
|
|
1076
1090
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1077
|
-
return typeof File !== "undefined" ? new File([bytes],
|
|
1091
|
+
return typeof File !== "undefined" ? new File([bytes], name, { type: item.type }) : bytes;
|
|
1078
1092
|
}
|
|
1079
1093
|
function DropDestination({ action, isTargeted, accepts, children }) {
|
|
1080
1094
|
return sb(
|
|
@@ -1176,6 +1190,34 @@ function FileExporterPresenter(spec) {
|
|
|
1176
1190
|
}, [presented, setPresented]);
|
|
1177
1191
|
return null;
|
|
1178
1192
|
}
|
|
1193
|
+
const ListSelectionContext = createContext(null);
|
|
1194
|
+
function Row({ tag, onDelete, deletable, onMove, movable, index, containerIndex, children }) {
|
|
1195
|
+
const selection = useContext(ListSelectionContext);
|
|
1196
|
+
const selectable = selection !== null && tag !== void 0;
|
|
1197
|
+
const canDelete = onDelete !== void 0 && deletable !== false;
|
|
1198
|
+
const canMove = onMove !== void 0 && movable !== false;
|
|
1199
|
+
if (!selectable && !canDelete && !canMove) return children;
|
|
1200
|
+
const props = {};
|
|
1201
|
+
if (selectable) {
|
|
1202
|
+
props["selectable"] = true;
|
|
1203
|
+
props["selected"] = selection.isSelected(tag);
|
|
1204
|
+
}
|
|
1205
|
+
if (canDelete) props["deletable"] = true;
|
|
1206
|
+
if (canMove) props["movable"] = true;
|
|
1207
|
+
return sb(
|
|
1208
|
+
"listrow",
|
|
1209
|
+
props,
|
|
1210
|
+
{
|
|
1211
|
+
tap: () => selectable && selection.select(tag),
|
|
1212
|
+
delete: () => onDelete?.(),
|
|
1213
|
+
move: (event) => {
|
|
1214
|
+
const start = (containerIndex ?? index ?? 0) - (index ?? 0);
|
|
1215
|
+
onMove?.(Math.max(0, event.to - start));
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
children
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1179
1221
|
const isPoint = (value) => typeof value === "object" && value !== null && ("x" in value || "y" in value);
|
|
1180
1222
|
function command(request) {
|
|
1181
1223
|
return request.axes ? { anchor: request.anchor, serial: request.serial, axes: request.axes } : { anchor: request.anchor, serial: request.serial };
|
|
@@ -1302,34 +1344,6 @@ function marginedContent(children, margins, axis) {
|
|
|
1302
1344
|
const content = list.length === 1 ? list[0] : sb(axis === "horizontal" ? "hstack" : "vstack", { spacing: null, alignment: "center" }, void 0, children);
|
|
1303
1345
|
return sb("styled", { style: { padding: margins } }, void 0, content);
|
|
1304
1346
|
}
|
|
1305
|
-
const ListSelectionContext = createContext(null);
|
|
1306
|
-
function Row({ tag, onDelete, deletable, onMove, movable, index, containerIndex, children }) {
|
|
1307
|
-
const selection = useContext(ListSelectionContext);
|
|
1308
|
-
const selectable = selection !== null && tag !== void 0;
|
|
1309
|
-
const canDelete = onDelete !== void 0 && deletable !== false;
|
|
1310
|
-
const canMove = onMove !== void 0 && movable !== false;
|
|
1311
|
-
if (!selectable && !canDelete && !canMove) return children;
|
|
1312
|
-
const props = {};
|
|
1313
|
-
if (selectable) {
|
|
1314
|
-
props["selectable"] = true;
|
|
1315
|
-
props["selected"] = selection.isSelected(tag);
|
|
1316
|
-
}
|
|
1317
|
-
if (canDelete) props["deletable"] = true;
|
|
1318
|
-
if (canMove) props["movable"] = true;
|
|
1319
|
-
return sb(
|
|
1320
|
-
"listrow",
|
|
1321
|
-
props,
|
|
1322
|
-
{
|
|
1323
|
-
tap: () => selectable && selection.select(tag),
|
|
1324
|
-
delete: () => onDelete?.(),
|
|
1325
|
-
move: (event) => {
|
|
1326
|
-
const start = (containerIndex ?? index ?? 0) - (index ?? 0);
|
|
1327
|
-
onMove?.(Math.max(0, event.to - start));
|
|
1328
|
-
}
|
|
1329
|
-
},
|
|
1330
|
-
children
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
1347
|
const LAZY_BATCH = 12;
|
|
1334
1348
|
const LazyContext = createContext(null);
|
|
1335
1349
|
function LazyGroup({ rows, start = 0, wrap: wrap2 }) {
|
|
@@ -1433,7 +1447,8 @@ function ScrollView(props) {
|
|
|
1433
1447
|
const scroll = { axes, ...refresh.props, ...geometry.props };
|
|
1434
1448
|
const handlers = refresh.handlers || geometry.handlers ? { ...refresh.handlers, ...geometry.handlers } : void 0;
|
|
1435
1449
|
if (own.scrollTargetBehavior) scroll["snap"] = own.scrollTargetBehavior;
|
|
1436
|
-
if (own.scrollDismissesKeyboard) scroll["dismissesKeyboard"] = true;
|
|
1450
|
+
if (own.scrollDismissesKeyboard === true || own.scrollDismissesKeyboard === "immediately") scroll["dismissesKeyboard"] = true;
|
|
1451
|
+
else if (own.scrollDismissesKeyboard === "interactively") scroll["dismissesKeyboard"] = "interactively";
|
|
1437
1452
|
if (own.scrollIndicators === "hidden") scroll["showsIndicators"] = false;
|
|
1438
1453
|
if (own.defaultScrollAnchor !== void 0) scroll["defaultScrollAnchor"] = unitPoint(own.defaultScrollAnchor);
|
|
1439
1454
|
const edgeEffect = edgeEffectProp(settings);
|
|
@@ -1799,13 +1814,20 @@ function NavigationLink(props) {
|
|
|
1799
1814
|
const navigation = useNavigation();
|
|
1800
1815
|
const environment = useViewEnvironment();
|
|
1801
1816
|
const label = own.title !== void 0 ? /* @__PURE__ */ jsx(Text, { children: own.title }) : own.children;
|
|
1817
|
+
const listSelection = useContext(ListSelectionContext);
|
|
1818
|
+
const sidebar = useContext(SplitSelectionContext) !== null && listSelection !== null && own.destination === void 0 && own.value !== void 0;
|
|
1802
1819
|
const tap = () => {
|
|
1820
|
+
if (sidebar) {
|
|
1821
|
+
listSelection.select(own.value);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1803
1824
|
if (!navigation) return;
|
|
1804
1825
|
if (own.destination !== void 0) navigation.push({ view: own.destination });
|
|
1805
1826
|
else navigation.push(own.value);
|
|
1806
1827
|
};
|
|
1807
1828
|
const style = takeModifier(modifiers, "buttonStyle") ?? environment.buttonStyle;
|
|
1808
1829
|
const linkProps = style === "plain" ? { style: "plain" } : {};
|
|
1830
|
+
if (sidebar) linkProps["selects"] = true;
|
|
1809
1831
|
return applyModifiers(sb("navlink", linkProps, { tap }, label), modifiers);
|
|
1810
1832
|
}
|
|
1811
1833
|
const SplitSelectionContext = createContext(null);
|
|
@@ -1814,8 +1836,8 @@ function NavigationSplitView(props) {
|
|
|
1814
1836
|
const sizeClass = useHorizontalSizeClass();
|
|
1815
1837
|
const [selected, setSelected] = useState(false);
|
|
1816
1838
|
const reporters = useRef(/* @__PURE__ */ new Map());
|
|
1839
|
+
const settle = () => setSelected([...reporters.current.values()].some((entry) => entry.selected));
|
|
1817
1840
|
const [report] = useState(() => {
|
|
1818
|
-
const settle = () => setSelected([...reporters.current.values()].some((entry) => entry.selected));
|
|
1819
1841
|
return {
|
|
1820
1842
|
report: (key, nowSelected, clear2) => {
|
|
1821
1843
|
reporters.current.set(key, { selected: nowSelected, clear: clear2 });
|
|
@@ -1827,15 +1849,17 @@ function NavigationSplitView(props) {
|
|
|
1827
1849
|
};
|
|
1828
1850
|
});
|
|
1829
1851
|
const clear = () => {
|
|
1830
|
-
for (const entry of reporters.current.values())
|
|
1852
|
+
for (const entry of reporters.current.values()) {
|
|
1853
|
+
if (!entry.selected) continue;
|
|
1854
|
+
entry.clear();
|
|
1855
|
+
entry.selected = false;
|
|
1856
|
+
}
|
|
1857
|
+
settle();
|
|
1831
1858
|
};
|
|
1832
1859
|
const [internal, setInternal] = useState("automatic");
|
|
1833
1860
|
const [visibility, setVisibility] = own.columnVisibility !== void 0 ? readBinding(own.columnVisibility, own.onColumnVisibilityChange) : [internal, setInternal];
|
|
1834
1861
|
if (sizeClass === "compact") {
|
|
1835
|
-
return applyModifiers(
|
|
1836
|
-
/* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: report, children: /* @__PURE__ */ jsx(CompactSplit, { sidebar: own.sidebar, detail: own.detail, selected, clear }) }),
|
|
1837
|
-
modifiers
|
|
1838
|
-
);
|
|
1862
|
+
return applyModifiers(/* @__PURE__ */ jsx(CompactSplit, { sidebar: own.sidebar, detail: own.detail, report, selected, clear }), modifiers);
|
|
1839
1863
|
}
|
|
1840
1864
|
const sidebarVisible = visibility !== "detailOnly";
|
|
1841
1865
|
const toggle = () => withAnimation(() => setVisibility(sidebarVisible ? "detailOnly" : "all"));
|
|
@@ -1851,11 +1875,17 @@ function NavigationSplitView(props) {
|
|
|
1851
1875
|
function SplitColumn({ report, children }) {
|
|
1852
1876
|
return /* @__PURE__ */ jsx(NavigationContext.Provider, { value: null, children: /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: report, children }) });
|
|
1853
1877
|
}
|
|
1854
|
-
function CompactSplit({
|
|
1878
|
+
function CompactSplit({
|
|
1879
|
+
sidebar,
|
|
1880
|
+
detail,
|
|
1881
|
+
report,
|
|
1882
|
+
selected,
|
|
1883
|
+
clear
|
|
1884
|
+
}) {
|
|
1855
1885
|
const [internal, setInternal] = useState([]);
|
|
1856
1886
|
const detailStack = isValidElement(detail) && detail.type === NavigationStack ? detail.props : null;
|
|
1857
1887
|
const [extra, setExtra] = detailStack?.path !== void 0 ? readBinding(detailStack.path, detailStack.onPathChange) : [internal, setInternal];
|
|
1858
|
-
const base = selected ? [{ view:
|
|
1888
|
+
const base = selected ? [{ view: detail }] : [];
|
|
1859
1889
|
const path = [...base, ...extra];
|
|
1860
1890
|
const setPath = (next) => {
|
|
1861
1891
|
if (selected && next.length === 0) {
|
|
@@ -1865,10 +1895,7 @@ function CompactSplit({ sidebar, detail, selected, clear }) {
|
|
|
1865
1895
|
}
|
|
1866
1896
|
setExtra(next.slice(base.length));
|
|
1867
1897
|
};
|
|
1868
|
-
return /* @__PURE__ */ jsx(NavigationContext.Provider, { value: null, children: /* @__PURE__ */ jsx(NavigationStack, { path: [path, setPath], children: sidebar }) });
|
|
1869
|
-
}
|
|
1870
|
-
function DetailScope({ children }) {
|
|
1871
|
-
return /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: null, children });
|
|
1898
|
+
return /* @__PURE__ */ jsx(NavigationContext.Provider, { value: null, children: /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: null, children: /* @__PURE__ */ jsx(NavigationStack, { path: [path, setPath], children: /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: report, children: sidebar }) }) }) });
|
|
1872
1899
|
}
|
|
1873
1900
|
function useReportSplitSelection(split, selected, clear, active2 = true) {
|
|
1874
1901
|
const key = useRef({}).current;
|
|
@@ -1877,6 +1904,9 @@ function useReportSplitSelection(split, selected, clear, active2 = true) {
|
|
|
1877
1904
|
else split?.retract(key);
|
|
1878
1905
|
});
|
|
1879
1906
|
useLayoutEffect(() => () => split?.retract(key), [split, key]);
|
|
1907
|
+
return (nowSelected, nowClear) => {
|
|
1908
|
+
if (active2) split?.report(key, nowSelected, nowClear);
|
|
1909
|
+
};
|
|
1880
1910
|
}
|
|
1881
1911
|
function OutsideSplit({ children }) {
|
|
1882
1912
|
return /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: null, children });
|
|
@@ -2156,6 +2186,7 @@ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
|
|
|
2156
2186
|
"cornerRadius",
|
|
2157
2187
|
"clipShape",
|
|
2158
2188
|
"clipped",
|
|
2189
|
+
"compositingGroup",
|
|
2159
2190
|
"opacity",
|
|
2160
2191
|
"disabled",
|
|
2161
2192
|
"hidden",
|
|
@@ -2185,6 +2216,8 @@ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
|
|
|
2185
2216
|
"navigationTitle",
|
|
2186
2217
|
"navigationBarTitleDisplayMode",
|
|
2187
2218
|
"toolbarRole",
|
|
2219
|
+
"toolbarVisibility",
|
|
2220
|
+
"statusBarHidden",
|
|
2188
2221
|
"toolbar",
|
|
2189
2222
|
"searchable",
|
|
2190
2223
|
"navigationDestination",
|
|
@@ -2259,6 +2292,7 @@ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
|
|
|
2259
2292
|
"onKeyPress",
|
|
2260
2293
|
"sensoryFeedback",
|
|
2261
2294
|
"textCase",
|
|
2295
|
+
"textSelection",
|
|
2262
2296
|
"coordinateSpace",
|
|
2263
2297
|
"contentTransition",
|
|
2264
2298
|
"scrollDisabled",
|
|
@@ -2401,6 +2435,7 @@ const STYLED_KEYS = /* @__PURE__ */ new Set([
|
|
|
2401
2435
|
"cornerRadius",
|
|
2402
2436
|
"clipShape",
|
|
2403
2437
|
"clipped",
|
|
2438
|
+
"compositingGroup",
|
|
2404
2439
|
"opacity",
|
|
2405
2440
|
"disabled",
|
|
2406
2441
|
"hidden",
|
|
@@ -2446,7 +2481,8 @@ const STYLED_KEYS = /* @__PURE__ */ new Set([
|
|
|
2446
2481
|
"matchedGeometryEffect",
|
|
2447
2482
|
"containerRelativeFrame",
|
|
2448
2483
|
"contentTransition",
|
|
2449
|
-
"scrollDisabled"
|
|
2484
|
+
"scrollDisabled",
|
|
2485
|
+
"textSelection"
|
|
2450
2486
|
]);
|
|
2451
2487
|
const ENVIRONMENT_KEYS = /* @__PURE__ */ new Set([
|
|
2452
2488
|
"buttonStyle",
|
|
@@ -2513,6 +2549,8 @@ function apply(key, value, node, sides) {
|
|
|
2513
2549
|
}
|
|
2514
2550
|
case "clipped":
|
|
2515
2551
|
return value ? styled({ clipped: true }, node) : node;
|
|
2552
|
+
case "compositingGroup":
|
|
2553
|
+
return value ? styled({ compositingGroup: true }, node) : node;
|
|
2516
2554
|
case "opacity":
|
|
2517
2555
|
return styled({ opacity: value }, node);
|
|
2518
2556
|
case "disabled":
|
|
@@ -2598,6 +2636,17 @@ function apply(key, value, node, sides) {
|
|
|
2598
2636
|
case "toolbarRole":
|
|
2599
2637
|
sides.push(sb(PREF_KIND, { toolbarRole: value }, void 0, void 0, "navscreen"));
|
|
2600
2638
|
return node;
|
|
2639
|
+
case "toolbarVisibility": {
|
|
2640
|
+
const bars = value;
|
|
2641
|
+
const props = {};
|
|
2642
|
+
if (bars.navigationBar && bars.navigationBar !== "automatic") props["navigationBarHidden"] = bars.navigationBar === "hidden";
|
|
2643
|
+
if (bars.bottomBar && bars.bottomBar !== "automatic") props["bottomBarHidden"] = bars.bottomBar === "hidden";
|
|
2644
|
+
sides.push(sb(PREF_KIND, props, void 0, void 0, "navscreen"));
|
|
2645
|
+
return node;
|
|
2646
|
+
}
|
|
2647
|
+
case "statusBarHidden":
|
|
2648
|
+
sides.push(sb(PREF_KIND, { statusBarHidden: Boolean(value) }, void 0, void 0, "navscreen"));
|
|
2649
|
+
return node;
|
|
2601
2650
|
case "toolbar":
|
|
2602
2651
|
sides.push(sb(HOIST_KIND, void 0, void 0, value, "navscreen"));
|
|
2603
2652
|
return node;
|
|
@@ -2631,8 +2680,8 @@ function apply(key, value, node, sides) {
|
|
|
2631
2680
|
const count = Math.floor(spec.count ?? 1);
|
|
2632
2681
|
const props = count > 1 ? { count } : {};
|
|
2633
2682
|
if ("perform" in spec) {
|
|
2634
|
-
const { perform } = spec;
|
|
2635
|
-
return sb("tapgesture", { ...props, location: true }, { tap: (event) =>
|
|
2683
|
+
const { perform: perform2 } = spec;
|
|
2684
|
+
return sb("tapgesture", { ...props, location: true }, { tap: (event) => perform2({ x: event.x ?? 0, y: event.y ?? 0 }) }, node);
|
|
2636
2685
|
}
|
|
2637
2686
|
return sb("tapgesture", props, { tap: () => spec.action() }, node);
|
|
2638
2687
|
}
|
|
@@ -2852,6 +2901,8 @@ function apply(key, value, node, sides) {
|
|
|
2852
2901
|
}
|
|
2853
2902
|
case "scrollDisabled":
|
|
2854
2903
|
return styled({ scrollDisabled: value === true }, node);
|
|
2904
|
+
case "textSelection":
|
|
2905
|
+
return styled({ textSelection: value === "enabled" }, node);
|
|
2855
2906
|
case "contentTransition": {
|
|
2856
2907
|
if (value === "identity") return node;
|
|
2857
2908
|
if (typeof value === "object") {
|
|
@@ -3574,6 +3625,848 @@ function useAudioRecorder(options = {}) {
|
|
|
3574
3625
|
);
|
|
3575
3626
|
return { available: host !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
|
|
3576
3627
|
}
|
|
3628
|
+
let nextId$1 = 1;
|
|
3629
|
+
const newId = () => nextId$1++;
|
|
3630
|
+
const hostNow$1 = () => browserEnvironment.audioEngine;
|
|
3631
|
+
function unsupported() {
|
|
3632
|
+
return new Error("unsupported: this page or app has no audio engine");
|
|
3633
|
+
}
|
|
3634
|
+
function perform$1(command2) {
|
|
3635
|
+
const host = hostNow$1();
|
|
3636
|
+
if (!host) return Promise.reject(unsupported());
|
|
3637
|
+
return host.perform(command2);
|
|
3638
|
+
}
|
|
3639
|
+
class AVAudioTime {
|
|
3640
|
+
spec;
|
|
3641
|
+
constructor(sampleTime, atRate) {
|
|
3642
|
+
this.spec = { sampleTime: Math.round(sampleTime), sampleRate: atRate };
|
|
3643
|
+
}
|
|
3644
|
+
static secondsFromNow(seconds) {
|
|
3645
|
+
const time = Object.create(AVAudioTime.prototype);
|
|
3646
|
+
time.spec = { delay: Math.max(0, seconds) };
|
|
3647
|
+
return time;
|
|
3648
|
+
}
|
|
3649
|
+
get isSampleTimeValid() {
|
|
3650
|
+
return "sampleTime" in this.spec;
|
|
3651
|
+
}
|
|
3652
|
+
get sampleTime() {
|
|
3653
|
+
return "sampleTime" in this.spec ? this.spec.sampleTime : 0;
|
|
3654
|
+
}
|
|
3655
|
+
get sampleRate() {
|
|
3656
|
+
return "sampleTime" in this.spec ? this.spec.sampleRate : 0;
|
|
3657
|
+
}
|
|
3658
|
+
/** The time in seconds: a sample time's place on its timeline, or the delay. */
|
|
3659
|
+
get seconds() {
|
|
3660
|
+
return "sampleTime" in this.spec ? this.spec.sampleTime / this.spec.sampleRate : this.spec.delay;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
const released = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((id) => void perform$1({ action: "release", id }).catch(() => void 0)) : null;
|
|
3664
|
+
class AVAudioFile {
|
|
3665
|
+
constructor(id, url, length, processingFormat) {
|
|
3666
|
+
this.id = id;
|
|
3667
|
+
this.url = url;
|
|
3668
|
+
this.length = length;
|
|
3669
|
+
this.processingFormat = processingFormat;
|
|
3670
|
+
released?.register(this, id, this);
|
|
3671
|
+
}
|
|
3672
|
+
id;
|
|
3673
|
+
url;
|
|
3674
|
+
length;
|
|
3675
|
+
processingFormat;
|
|
3676
|
+
/** Where the next `read` starts, in frames. */
|
|
3677
|
+
framePosition = 0;
|
|
3678
|
+
/** Opens a file for reading; rejects with `notFound: …` or `decode: …`. */
|
|
3679
|
+
static async open(url) {
|
|
3680
|
+
const id = newId();
|
|
3681
|
+
const info = await perform$1({ action: "openFile", file: id, url });
|
|
3682
|
+
return new AVAudioFile(id, url, info.length, { sampleRate: info.sampleRate, channelCount: info.channelCount });
|
|
3683
|
+
}
|
|
3684
|
+
/** Seconds. */
|
|
3685
|
+
get duration() {
|
|
3686
|
+
return this.length / this.processingFormat.sampleRate;
|
|
3687
|
+
}
|
|
3688
|
+
/** Reads frames from `framePosition` into `buffer` (up to its capacity, or `frameCount`) and moves past them. */
|
|
3689
|
+
async read(into, frameCount) {
|
|
3690
|
+
const frames = Math.max(0, Math.min(frameCount ?? into.frameCapacity, into.frameCapacity, this.length - this.framePosition));
|
|
3691
|
+
const info = await perform$1({ action: "readFile", file: this.id, buffer: into.id, start: this.framePosition, frames });
|
|
3692
|
+
into.frameLength = info.frameLength;
|
|
3693
|
+
into.format = { sampleRate: info.sampleRate, channelCount: info.channelCount };
|
|
3694
|
+
this.framePosition += info.frameLength;
|
|
3695
|
+
}
|
|
3696
|
+
/** Lets the host free the decoded audio (what a scheduled segment still plays keeps playing). */
|
|
3697
|
+
close() {
|
|
3698
|
+
released?.unregister(this);
|
|
3699
|
+
void perform$1({ action: "release", id: this.id }).catch(() => void 0);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
class AVAudioPCMBuffer {
|
|
3703
|
+
constructor(format, frameCapacity) {
|
|
3704
|
+
this.format = format;
|
|
3705
|
+
this.frameCapacity = frameCapacity;
|
|
3706
|
+
released?.register(this, this.id, this);
|
|
3707
|
+
}
|
|
3708
|
+
format;
|
|
3709
|
+
frameCapacity;
|
|
3710
|
+
id = newId();
|
|
3711
|
+
frameLength = 0;
|
|
3712
|
+
/** Seconds of audio it holds. */
|
|
3713
|
+
get duration() {
|
|
3714
|
+
return this.frameLength / this.format.sampleRate;
|
|
3715
|
+
}
|
|
3716
|
+
/** Fills the buffer with the app's samples, one array per channel (up to its capacity). */
|
|
3717
|
+
async write(channels) {
|
|
3718
|
+
const frames = Math.min(this.frameCapacity, ...channels.map((data) => data.length));
|
|
3719
|
+
await perform$1({
|
|
3720
|
+
action: "writeBuffer",
|
|
3721
|
+
buffer: this.id,
|
|
3722
|
+
sampleRate: this.format.sampleRate,
|
|
3723
|
+
channels: channels.map((data) => data.length === frames ? data : data.subarray(0, frames))
|
|
3724
|
+
});
|
|
3725
|
+
this.frameLength = frames;
|
|
3726
|
+
this.format = { sampleRate: this.format.sampleRate, channelCount: channels.length };
|
|
3727
|
+
}
|
|
3728
|
+
/** Lets the host free the audio. */
|
|
3729
|
+
close() {
|
|
3730
|
+
released?.unregister(this);
|
|
3731
|
+
void perform$1({ action: "release", id: this.id }).catch(() => void 0);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
class AVAudioNode {
|
|
3735
|
+
id = newId();
|
|
3736
|
+
/** The engine it is attached to, or null. */
|
|
3737
|
+
engine = null;
|
|
3738
|
+
values = {};
|
|
3739
|
+
get(key, fallback) {
|
|
3740
|
+
return this.values[key] ?? fallback;
|
|
3741
|
+
}
|
|
3742
|
+
set(key, value) {
|
|
3743
|
+
this.values[key] = value;
|
|
3744
|
+
this.engine?.fire({ action: "set", node: this.id, values: { [key]: value } });
|
|
3745
|
+
}
|
|
3746
|
+
/** The properties to attach it with. */
|
|
3747
|
+
attachValues() {
|
|
3748
|
+
return { ...this.values };
|
|
3749
|
+
}
|
|
3750
|
+
/** How many EQ bands it has (only an EQ has any). */
|
|
3751
|
+
get bandCount() {
|
|
3752
|
+
return void 0;
|
|
3753
|
+
}
|
|
3754
|
+
/** What its engine must be for a call to reach it. */
|
|
3755
|
+
attached() {
|
|
3756
|
+
if (!this.engine) throw new Error("notAttached: attach the node to an engine first");
|
|
3757
|
+
return this.engine;
|
|
3758
|
+
}
|
|
3759
|
+
/** Its output's level now (0 silent … 1 at 0 dB, from -50 dB), as a tap on it would measure; 0 when not attached. */
|
|
3760
|
+
async meter() {
|
|
3761
|
+
if (!this.engine) return 0;
|
|
3762
|
+
const result = await this.engine.send({ action: "meter", node: this.id });
|
|
3763
|
+
return typeof result?.level === "number" ? result.level : 0;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
class MixingNode extends AVAudioNode {
|
|
3767
|
+
get volume() {
|
|
3768
|
+
return this.get("volume", 1);
|
|
3769
|
+
}
|
|
3770
|
+
set volume(value) {
|
|
3771
|
+
this.set("volume", value);
|
|
3772
|
+
}
|
|
3773
|
+
get pan() {
|
|
3774
|
+
return this.get("pan", 0);
|
|
3775
|
+
}
|
|
3776
|
+
set pan(value) {
|
|
3777
|
+
this.set("pan", value);
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
class AVAudioMixerNode extends MixingNode {
|
|
3781
|
+
kind = "mixer";
|
|
3782
|
+
get outputVolume() {
|
|
3783
|
+
return this.get("outputVolume", 1);
|
|
3784
|
+
}
|
|
3785
|
+
set outputVolume(value) {
|
|
3786
|
+
this.set("outputVolume", value);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
class AVAudioOutputNode extends AVAudioNode {
|
|
3790
|
+
kind = null;
|
|
3791
|
+
}
|
|
3792
|
+
class AVAudioInputNode extends MixingNode {
|
|
3793
|
+
kind = null;
|
|
3794
|
+
}
|
|
3795
|
+
class AVAudioPlayerNode extends MixingNode {
|
|
3796
|
+
kind = "player";
|
|
3797
|
+
playing = false;
|
|
3798
|
+
get isPlaying() {
|
|
3799
|
+
return this.playing && this.engine?.isRunning === true;
|
|
3800
|
+
}
|
|
3801
|
+
scheduleFile(file, at = null, completionHandler) {
|
|
3802
|
+
return this.schedule({ action: "scheduleFile", node: this.id, file: file.id, ...at ? { at: at.spec } : {} }, completionHandler);
|
|
3803
|
+
}
|
|
3804
|
+
scheduleSegment(file, startingFrame, frameCount, at = null, completionHandler) {
|
|
3805
|
+
return this.schedule(
|
|
3806
|
+
{
|
|
3807
|
+
action: "scheduleFile",
|
|
3808
|
+
node: this.id,
|
|
3809
|
+
file: file.id,
|
|
3810
|
+
start: Math.max(0, Math.floor(startingFrame)),
|
|
3811
|
+
frames: Math.max(0, Math.floor(frameCount)),
|
|
3812
|
+
...at ? { at: at.spec } : {}
|
|
3813
|
+
},
|
|
3814
|
+
completionHandler
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3817
|
+
scheduleBuffer(buffer, at = null, options = {}, completionHandler) {
|
|
3818
|
+
const command2 = {
|
|
3819
|
+
action: "scheduleBuffer",
|
|
3820
|
+
node: this.id,
|
|
3821
|
+
buffer: buffer.id,
|
|
3822
|
+
...at ? { at: at.spec } : {}
|
|
3823
|
+
};
|
|
3824
|
+
if (options.loops) command2.loops = true;
|
|
3825
|
+
if (options.interruptsAtLoop) command2.interruptsAtLoop = true;
|
|
3826
|
+
else if (options.interrupts) command2.interrupts = true;
|
|
3827
|
+
return this.schedule(command2, completionHandler);
|
|
3828
|
+
}
|
|
3829
|
+
schedule(command2, completionHandler) {
|
|
3830
|
+
const engine = this.attached();
|
|
3831
|
+
return engine.send(command2).then(() => completionHandler?.());
|
|
3832
|
+
}
|
|
3833
|
+
/** Starts (or resumes) the timeline, now or at `at`; the engine must be running. */
|
|
3834
|
+
play(at = null) {
|
|
3835
|
+
const engine = this.attached();
|
|
3836
|
+
this.playing = true;
|
|
3837
|
+
engine.fire({ action: "play", node: this.id, ...at ? { at: at.spec } : {} });
|
|
3838
|
+
}
|
|
3839
|
+
/** Holds the timeline where it is; `play()` goes on from there. */
|
|
3840
|
+
pause() {
|
|
3841
|
+
this.playing = false;
|
|
3842
|
+
this.engine?.fire({ action: "pausePlayer", node: this.id });
|
|
3843
|
+
}
|
|
3844
|
+
/** Drops everything scheduled (each completion called) and returns the timeline to 0. */
|
|
3845
|
+
stop() {
|
|
3846
|
+
this.playing = false;
|
|
3847
|
+
this.engine?.fire({ action: "stopPlayer", node: this.id });
|
|
3848
|
+
}
|
|
3849
|
+
/** Where the timeline is now (`playerTime(forNodeTime: lastRenderTime)`), or null before it plays. */
|
|
3850
|
+
async playerTime() {
|
|
3851
|
+
const time = await this.attached().send({ action: "playerTime", node: this.id });
|
|
3852
|
+
return time && typeof time.sampleTime === "number" ? new AVAudioTime(time.sampleTime, time.sampleRate) : null;
|
|
3853
|
+
}
|
|
3854
|
+
/** Called by the engine when it stops: the player stops with it. */
|
|
3855
|
+
engineStopped() {
|
|
3856
|
+
this.playing = false;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const NOTE_CLASSES = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
3860
|
+
function noteInName(url) {
|
|
3861
|
+
const name = decodeURIComponent(url.split(/[/\\]/).pop() ?? "").replace(/\.[^.]*$/, "");
|
|
3862
|
+
const named = [...name.matchAll(/(?:^|[^A-Za-z])([A-Ga-g])([#♯]|b|♭)?(-?\d)(?!\d)/g)].pop();
|
|
3863
|
+
if (named) {
|
|
3864
|
+
const pitchClass = NOTE_CLASSES[named[1].toLowerCase()];
|
|
3865
|
+
const accidental = named[2] === "#" || named[2] === "♯" ? 1 : named[2] ? -1 : 0;
|
|
3866
|
+
const note = (Number(named[3]) + 1) * 12 + pitchClass + accidental;
|
|
3867
|
+
return note >= 0 && note <= 127 ? note : null;
|
|
3868
|
+
}
|
|
3869
|
+
const numbered = [...name.matchAll(/(?:^|\D)(\d{1,3})(?!\d)/g)].pop();
|
|
3870
|
+
if (numbered) {
|
|
3871
|
+
const note = Number(numbered[1]);
|
|
3872
|
+
return note <= 127 ? note : null;
|
|
3873
|
+
}
|
|
3874
|
+
return null;
|
|
3875
|
+
}
|
|
3876
|
+
function samplerZones(samples) {
|
|
3877
|
+
const sorted = [...samples].sort((a, b) => a.root - b.root).filter((sample, i, all) => i === 0 || sample.root !== all[i - 1].root);
|
|
3878
|
+
return sorted.map((sample, i) => {
|
|
3879
|
+
const below = sorted[i - 1];
|
|
3880
|
+
const above = sorted[i + 1];
|
|
3881
|
+
return {
|
|
3882
|
+
file: sample.file,
|
|
3883
|
+
root: sample.root,
|
|
3884
|
+
low: below ? Math.floor((below.root + sample.root) / 2) + 1 : 0,
|
|
3885
|
+
high: above ? Math.floor((sample.root + above.root) / 2) : 127
|
|
3886
|
+
};
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
class AVAudioUnitSampler extends MixingNode {
|
|
3890
|
+
kind = "sampler";
|
|
3891
|
+
files = [];
|
|
3892
|
+
/** Decibels, -90 … 12. */
|
|
3893
|
+
get overallGain() {
|
|
3894
|
+
return this.get("overallGain", 0);
|
|
3895
|
+
}
|
|
3896
|
+
set overallGain(value) {
|
|
3897
|
+
this.set("overallGain", value);
|
|
3898
|
+
}
|
|
3899
|
+
/** Cents, -2400 … 2400. */
|
|
3900
|
+
get globalTuning() {
|
|
3901
|
+
return this.get("globalTuning", 0);
|
|
3902
|
+
}
|
|
3903
|
+
set globalTuning(value) {
|
|
3904
|
+
this.set("globalTuning", value);
|
|
3905
|
+
}
|
|
3906
|
+
/** -100 left … 100 right. */
|
|
3907
|
+
get stereoPan() {
|
|
3908
|
+
return this.get("stereoPan", 0);
|
|
3909
|
+
}
|
|
3910
|
+
set stereoPan(value) {
|
|
3911
|
+
this.set("stereoPan", value);
|
|
3912
|
+
}
|
|
3913
|
+
async loadAudioFiles(urls, options = {}) {
|
|
3914
|
+
const engine = this.attached();
|
|
3915
|
+
const roots2 = urls.map((url, i) => options.rootNotes?.[i] ?? noteInName(url) ?? 60);
|
|
3916
|
+
const files = await Promise.all(urls.map((url) => AVAudioFile.open(url)));
|
|
3917
|
+
const zones = samplerZones(files.map((file, i) => ({ file: file.id, root: roots2[i] })));
|
|
3918
|
+
await engine.send({ action: "loadZones", node: this.id, zones, attack: options.attack ?? 2e-3, release: options.release ?? 0.15 });
|
|
3919
|
+
const previous = this.files;
|
|
3920
|
+
this.files = files;
|
|
3921
|
+
for (const file of previous) file.close();
|
|
3922
|
+
}
|
|
3923
|
+
/** Loads an `.aupreset`, a DLS or a SoundFont's first instrument (natively; a page rejects with `unsupported: …`). */
|
|
3924
|
+
async loadInstrument(url) {
|
|
3925
|
+
await this.attached().send({ action: "loadInstrument", node: this.id, url });
|
|
3926
|
+
}
|
|
3927
|
+
/** Loads one instrument of a SoundFont or DLS bank by program and bank (natively; a page rejects with `unsupported: …`). */
|
|
3928
|
+
async loadSoundBankInstrument(url, program, bankMSB = 121, bankLSB = 0) {
|
|
3929
|
+
await this.attached().send({ action: "loadInstrument", node: this.id, url, program, bankMSB, bankLSB });
|
|
3930
|
+
}
|
|
3931
|
+
startNote(note, withVelocity, onChannel = 0) {
|
|
3932
|
+
this.attached().fire({ action: "startNote", node: this.id, note: Math.round(note), velocity: Math.round(withVelocity), channel: onChannel });
|
|
3933
|
+
}
|
|
3934
|
+
stopNote(note, onChannel = 0) {
|
|
3935
|
+
this.attached().fire({ action: "stopNote", node: this.id, note: Math.round(note), channel: onChannel });
|
|
3936
|
+
}
|
|
3937
|
+
/** A MIDI controller: 64 the sustain pedal, 7 volume, 11 expression, 10 pan, 123 all notes off. */
|
|
3938
|
+
sendController(controller, withValue, onChannel = 0) {
|
|
3939
|
+
this.attached().fire({ action: "controller", node: this.id, controller, value: Math.round(withValue), channel: onChannel });
|
|
3940
|
+
}
|
|
3941
|
+
/** The pitch wheel: 0 … 16383, 8192 centered (±2 semitones). */
|
|
3942
|
+
sendPitchBend(pitchbend, onChannel = 0) {
|
|
3943
|
+
this.attached().fire({ action: "pitchBend", node: this.id, value: Math.round(pitchbend), channel: onChannel });
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
class AVAudioUnitEffect extends AVAudioNode {
|
|
3947
|
+
get bypass() {
|
|
3948
|
+
return this.get("bypass", false);
|
|
3949
|
+
}
|
|
3950
|
+
set bypass(value) {
|
|
3951
|
+
this.set("bypass", value);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
class AVAudioUnitReverb extends AVAudioUnitEffect {
|
|
3955
|
+
kind = "reverb";
|
|
3956
|
+
constructor() {
|
|
3957
|
+
super();
|
|
3958
|
+
this.values["preset"] = "mediumHall";
|
|
3959
|
+
this.values["wetDryMix"] = 100;
|
|
3960
|
+
}
|
|
3961
|
+
get wetDryMix() {
|
|
3962
|
+
return this.get("wetDryMix", 100);
|
|
3963
|
+
}
|
|
3964
|
+
set wetDryMix(value) {
|
|
3965
|
+
this.set("wetDryMix", value);
|
|
3966
|
+
}
|
|
3967
|
+
loadFactoryPreset(preset) {
|
|
3968
|
+
this.values["preset"] = preset;
|
|
3969
|
+
this.engine?.fire({ action: "preset", node: this.id, preset });
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
class AVAudioUnitDelay extends AVAudioUnitEffect {
|
|
3973
|
+
kind = "delay";
|
|
3974
|
+
constructor() {
|
|
3975
|
+
super();
|
|
3976
|
+
Object.assign(this.values, { delayTime: 1, feedback: 50, lowPassCutoff: 15e3, wetDryMix: 100 });
|
|
3977
|
+
}
|
|
3978
|
+
get delayTime() {
|
|
3979
|
+
return this.get("delayTime", 1);
|
|
3980
|
+
}
|
|
3981
|
+
set delayTime(value) {
|
|
3982
|
+
this.set("delayTime", value);
|
|
3983
|
+
}
|
|
3984
|
+
get feedback() {
|
|
3985
|
+
return this.get("feedback", 50);
|
|
3986
|
+
}
|
|
3987
|
+
set feedback(value) {
|
|
3988
|
+
this.set("feedback", value);
|
|
3989
|
+
}
|
|
3990
|
+
get lowPassCutoff() {
|
|
3991
|
+
return this.get("lowPassCutoff", 15e3);
|
|
3992
|
+
}
|
|
3993
|
+
set lowPassCutoff(value) {
|
|
3994
|
+
this.set("lowPassCutoff", value);
|
|
3995
|
+
}
|
|
3996
|
+
get wetDryMix() {
|
|
3997
|
+
return this.get("wetDryMix", 100);
|
|
3998
|
+
}
|
|
3999
|
+
set wetDryMix(value) {
|
|
4000
|
+
this.set("wetDryMix", value);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
class AVAudioUnitDistortion extends AVAudioUnitEffect {
|
|
4004
|
+
kind = "distortion";
|
|
4005
|
+
constructor() {
|
|
4006
|
+
super();
|
|
4007
|
+
Object.assign(this.values, { preGain: -6, wetDryMix: 50 });
|
|
4008
|
+
}
|
|
4009
|
+
get preGain() {
|
|
4010
|
+
return this.get("preGain", -6);
|
|
4011
|
+
}
|
|
4012
|
+
set preGain(value) {
|
|
4013
|
+
this.set("preGain", value);
|
|
4014
|
+
}
|
|
4015
|
+
get wetDryMix() {
|
|
4016
|
+
return this.get("wetDryMix", 50);
|
|
4017
|
+
}
|
|
4018
|
+
set wetDryMix(value) {
|
|
4019
|
+
this.set("wetDryMix", value);
|
|
4020
|
+
}
|
|
4021
|
+
loadFactoryPreset(preset) {
|
|
4022
|
+
this.values["preset"] = preset;
|
|
4023
|
+
this.engine?.fire({ action: "preset", node: this.id, preset });
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
class AVAudioUnitEQFilterParameters {
|
|
4027
|
+
constructor(eq, index) {
|
|
4028
|
+
this.eq = eq;
|
|
4029
|
+
this.index = index;
|
|
4030
|
+
}
|
|
4031
|
+
eq;
|
|
4032
|
+
index;
|
|
4033
|
+
key(field) {
|
|
4034
|
+
return `bands.${this.index}.${field}`;
|
|
4035
|
+
}
|
|
4036
|
+
get filterType() {
|
|
4037
|
+
return this.eq.band(this.key("filterType"), "parametric");
|
|
4038
|
+
}
|
|
4039
|
+
set filterType(value) {
|
|
4040
|
+
this.eq.setBand(this.key("filterType"), value);
|
|
4041
|
+
}
|
|
4042
|
+
/** Hz. */
|
|
4043
|
+
get frequency() {
|
|
4044
|
+
return this.eq.band(this.key("frequency"), 1e3);
|
|
4045
|
+
}
|
|
4046
|
+
set frequency(value) {
|
|
4047
|
+
this.eq.setBand(this.key("frequency"), value);
|
|
4048
|
+
}
|
|
4049
|
+
/** Octaves (0.05 … 5). */
|
|
4050
|
+
get bandwidth() {
|
|
4051
|
+
return this.eq.band(this.key("bandwidth"), 0.5);
|
|
4052
|
+
}
|
|
4053
|
+
set bandwidth(value) {
|
|
4054
|
+
this.eq.setBand(this.key("bandwidth"), value);
|
|
4055
|
+
}
|
|
4056
|
+
/** Decibels (-96 … 24). */
|
|
4057
|
+
get gain() {
|
|
4058
|
+
return this.eq.band(this.key("gain"), 0);
|
|
4059
|
+
}
|
|
4060
|
+
set gain(value) {
|
|
4061
|
+
this.eq.setBand(this.key("gain"), value);
|
|
4062
|
+
}
|
|
4063
|
+
get bypass() {
|
|
4064
|
+
return this.eq.band(this.key("bypass"), true);
|
|
4065
|
+
}
|
|
4066
|
+
set bypass(value) {
|
|
4067
|
+
this.eq.setBand(this.key("bypass"), value);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
class AVAudioUnitEQ extends AVAudioUnitEffect {
|
|
4071
|
+
kind = "eq";
|
|
4072
|
+
bands;
|
|
4073
|
+
constructor(numberOfBands = 1) {
|
|
4074
|
+
super();
|
|
4075
|
+
const count = Math.max(1, Math.min(32, Math.floor(numberOfBands)));
|
|
4076
|
+
this.bands = Array.from({ length: count }, (_, i) => new AVAudioUnitEQFilterParameters(this, i));
|
|
4077
|
+
for (let i = 0; i < count; i += 1) {
|
|
4078
|
+
Object.assign(this.values, {
|
|
4079
|
+
[`bands.${i}.filterType`]: "parametric",
|
|
4080
|
+
[`bands.${i}.frequency`]: 1e3,
|
|
4081
|
+
[`bands.${i}.bandwidth`]: 0.5,
|
|
4082
|
+
[`bands.${i}.gain`]: 0,
|
|
4083
|
+
[`bands.${i}.bypass`]: true
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
get bandCount() {
|
|
4088
|
+
return this.bands.length;
|
|
4089
|
+
}
|
|
4090
|
+
get globalGain() {
|
|
4091
|
+
return this.get("globalGain", 0);
|
|
4092
|
+
}
|
|
4093
|
+
set globalGain(value) {
|
|
4094
|
+
this.set("globalGain", value);
|
|
4095
|
+
}
|
|
4096
|
+
/** A band's field (for `AVAudioUnitEQFilterParameters`). */
|
|
4097
|
+
band(key, fallback) {
|
|
4098
|
+
return this.values[key] ?? fallback;
|
|
4099
|
+
}
|
|
4100
|
+
setBand(key, value) {
|
|
4101
|
+
this.set(key, value);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
class AVAudioUnitTimePitch extends AVAudioUnitEffect {
|
|
4105
|
+
kind = "timePitch";
|
|
4106
|
+
get pitch() {
|
|
4107
|
+
return this.get("pitch", 0);
|
|
4108
|
+
}
|
|
4109
|
+
set pitch(value) {
|
|
4110
|
+
this.set("pitch", value);
|
|
4111
|
+
}
|
|
4112
|
+
get rate() {
|
|
4113
|
+
return this.get("rate", 1);
|
|
4114
|
+
}
|
|
4115
|
+
set rate(value) {
|
|
4116
|
+
this.set("rate", value);
|
|
4117
|
+
}
|
|
4118
|
+
get overlap() {
|
|
4119
|
+
return this.get("overlap", 8);
|
|
4120
|
+
}
|
|
4121
|
+
set overlap(value) {
|
|
4122
|
+
this.set("overlap", value);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
const engines = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((id) => void perform$1({ action: "destroy", engine: id }).catch(() => void 0)) : null;
|
|
4126
|
+
class AVAudioEngine {
|
|
4127
|
+
id = newId();
|
|
4128
|
+
mainMixerNode = new AVAudioMixerNode();
|
|
4129
|
+
outputNode = new AVAudioOutputNode();
|
|
4130
|
+
inputNode = new AVAudioInputNode();
|
|
4131
|
+
attachedNodes = /* @__PURE__ */ new Set();
|
|
4132
|
+
/** Where failures of calls that answer nothing go (a console warning by default). */
|
|
4133
|
+
onError = (error) => console.warn(`AVAudioEngine: ${error.message}`);
|
|
4134
|
+
running = false;
|
|
4135
|
+
disposed = false;
|
|
4136
|
+
/** Whether this page or app has an audio engine. */
|
|
4137
|
+
static get isAvailable() {
|
|
4138
|
+
return hostNow$1() !== void 0;
|
|
4139
|
+
}
|
|
4140
|
+
constructor() {
|
|
4141
|
+
for (const node of [this.mainMixerNode, this.outputNode, this.inputNode]) {
|
|
4142
|
+
node.engine = this;
|
|
4143
|
+
this.attachedNodes.add(node);
|
|
4144
|
+
}
|
|
4145
|
+
this.fire({ action: "create", engine: this.id, mixer: this.mainMixerNode.id, output: this.outputNode.id, input: this.inputNode.id });
|
|
4146
|
+
engines?.register(this, this.id, this);
|
|
4147
|
+
}
|
|
4148
|
+
get isRunning() {
|
|
4149
|
+
return this.running;
|
|
4150
|
+
}
|
|
4151
|
+
/** Sends a command and answers its result. */
|
|
4152
|
+
send(command2) {
|
|
4153
|
+
if (this.disposed) return Promise.reject(new Error("disposed: the engine was disposed"));
|
|
4154
|
+
return perform$1(command2);
|
|
4155
|
+
}
|
|
4156
|
+
/** Sends a command that answers nothing; a failure goes to `onError`. */
|
|
4157
|
+
fire(command2) {
|
|
4158
|
+
this.send(command2).catch((error) => this.onError(error instanceof Error ? error : new Error(String(error))));
|
|
4159
|
+
}
|
|
4160
|
+
attach(node) {
|
|
4161
|
+
if (node.engine === this) return;
|
|
4162
|
+
if (node.engine) throw new Error("attached: the node is attached to another engine");
|
|
4163
|
+
if (!node.kind) throw new Error("unsupported: an engine makes its own mixer, output and input");
|
|
4164
|
+
node.engine = this;
|
|
4165
|
+
this.attachedNodes.add(node);
|
|
4166
|
+
const bands = node.bandCount;
|
|
4167
|
+
this.fire({ action: "attach", engine: this.id, node: node.id, kind: node.kind, values: node.attachValues(), ...bands !== void 0 ? { bands } : {} });
|
|
4168
|
+
}
|
|
4169
|
+
detach(node) {
|
|
4170
|
+
if (node.engine !== this || !node.kind) return;
|
|
4171
|
+
this.fire({ action: "detach", engine: this.id, node: node.id });
|
|
4172
|
+
if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4173
|
+
node.engine = null;
|
|
4174
|
+
this.attachedNodes.delete(node);
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Connects `from`'s output to `to` (an array: to each of them), replacing
|
|
4178
|
+
* where it went before; a node that is not a mixer takes one input, so
|
|
4179
|
+
* what was connected into it is let go. `format` is accepted for Swift's
|
|
4180
|
+
* shape: the host picks one that fits both ends.
|
|
4181
|
+
*/
|
|
4182
|
+
connect(from, to, format = null) {
|
|
4183
|
+
const destinations = Array.isArray(to) ? to : [to];
|
|
4184
|
+
for (const node of [from, ...destinations]) {
|
|
4185
|
+
if (node.engine !== this) throw new Error("notAttached: attach both nodes to the engine before connecting them");
|
|
4186
|
+
}
|
|
4187
|
+
this.fire({ action: "connect", engine: this.id, from: from.id, to: destinations.map((node) => node.id) });
|
|
4188
|
+
}
|
|
4189
|
+
disconnectNodeOutput(node) {
|
|
4190
|
+
this.fire({ action: "disconnectOutput", engine: this.id, node: node.id });
|
|
4191
|
+
}
|
|
4192
|
+
disconnectNodeInput(node) {
|
|
4193
|
+
this.fire({ action: "disconnectInput", engine: this.id, node: node.id });
|
|
4194
|
+
}
|
|
4195
|
+
prepare() {
|
|
4196
|
+
this.fire({ action: "prepare", engine: this.id });
|
|
4197
|
+
}
|
|
4198
|
+
/** Starts rendering; with the input connected, asks for the microphone (rejecting `permission: …` when refused). */
|
|
4199
|
+
async start() {
|
|
4200
|
+
await this.send({ action: "start", engine: this.id });
|
|
4201
|
+
this.running = true;
|
|
4202
|
+
}
|
|
4203
|
+
/** Pauses rendering; players hold their place and play on at `start()`. */
|
|
4204
|
+
pause() {
|
|
4205
|
+
this.running = false;
|
|
4206
|
+
this.fire({ action: "pause", engine: this.id });
|
|
4207
|
+
}
|
|
4208
|
+
/** Stops rendering; players stop and drop what they had scheduled. */
|
|
4209
|
+
stop() {
|
|
4210
|
+
this.running = false;
|
|
4211
|
+
for (const node of this.attachedNodes) if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4212
|
+
this.fire({ action: "stop", engine: this.id });
|
|
4213
|
+
}
|
|
4214
|
+
/** Silences every node (players stop, notes end) without stopping the engine. */
|
|
4215
|
+
reset() {
|
|
4216
|
+
for (const node of this.attachedNodes) if (node instanceof AVAudioPlayerNode) node.engineStopped();
|
|
4217
|
+
this.fire({ action: "reset", engine: this.id });
|
|
4218
|
+
}
|
|
4219
|
+
/** Stops the engine and frees it on the host; it cannot be used after. */
|
|
4220
|
+
dispose() {
|
|
4221
|
+
if (this.disposed) return;
|
|
4222
|
+
engines?.unregister(this);
|
|
4223
|
+
void perform$1({ action: "destroy", engine: this.id }).catch(() => void 0);
|
|
4224
|
+
this.disposed = true;
|
|
4225
|
+
this.running = false;
|
|
4226
|
+
for (const node of this.attachedNodes) node.engine = null;
|
|
4227
|
+
this.attachedNodes.clear();
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
function useAudioEngine(build) {
|
|
4231
|
+
const [engine] = useState(() => {
|
|
4232
|
+
if (!AVAudioEngine.isAvailable) return null;
|
|
4233
|
+
const made = new AVAudioEngine();
|
|
4234
|
+
build?.(made);
|
|
4235
|
+
return made;
|
|
4236
|
+
});
|
|
4237
|
+
useEffect(() => () => engine?.dispose(), [engine]);
|
|
4238
|
+
return engine;
|
|
4239
|
+
}
|
|
4240
|
+
let nextId = 1;
|
|
4241
|
+
const hostNow = () => browserEnvironment.languageModel;
|
|
4242
|
+
function perform(command2) {
|
|
4243
|
+
const host = hostNow();
|
|
4244
|
+
if (!host) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
|
|
4245
|
+
return host.perform(command2).catch((error) => {
|
|
4246
|
+
throw GenerationError.from(error);
|
|
4247
|
+
});
|
|
4248
|
+
}
|
|
4249
|
+
const KINDS = {
|
|
4250
|
+
unavailable: "assetsUnavailable",
|
|
4251
|
+
context: "exceededContextWindowSize",
|
|
4252
|
+
guardrail: "guardrailViolation",
|
|
4253
|
+
language: "unsupportedLanguageOrLocale",
|
|
4254
|
+
concurrent: "concurrentRequests",
|
|
4255
|
+
rateLimited: "rateLimited",
|
|
4256
|
+
refusal: "refusal"
|
|
4257
|
+
};
|
|
4258
|
+
class GenerationError extends Error {
|
|
4259
|
+
constructor(kind, message) {
|
|
4260
|
+
super(message);
|
|
4261
|
+
this.kind = kind;
|
|
4262
|
+
this.name = "GenerationError";
|
|
4263
|
+
}
|
|
4264
|
+
kind;
|
|
4265
|
+
/** The error a host's tagged message (`context: …`) stands for. */
|
|
4266
|
+
static from(error) {
|
|
4267
|
+
if (error instanceof GenerationError) return error;
|
|
4268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4269
|
+
const match = /^(\w+): ?([\s\S]*)$/.exec(message);
|
|
4270
|
+
const kind = match ? KINDS[match[1]] : void 0;
|
|
4271
|
+
return kind ? new GenerationError(kind, match[2]) : new GenerationError("unknown", message);
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
class SystemLanguageModel {
|
|
4275
|
+
static default = new SystemLanguageModel();
|
|
4276
|
+
constructor() {
|
|
4277
|
+
}
|
|
4278
|
+
/** Views showing the availability, asked again when a download starts or the model goes. */
|
|
4279
|
+
listeners = /* @__PURE__ */ new Set();
|
|
4280
|
+
/** Whether the model can answer now, or why not. */
|
|
4281
|
+
async availability() {
|
|
4282
|
+
if (!hostNow()) return { status: "unavailable", reason: "deviceNotEligible" };
|
|
4283
|
+
try {
|
|
4284
|
+
return await perform({ action: "availability" });
|
|
4285
|
+
} catch {
|
|
4286
|
+
return { status: "unavailable", reason: "deviceNotEligible" };
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
async isAvailable() {
|
|
4290
|
+
return (await this.availability()).status === "available";
|
|
4291
|
+
}
|
|
4292
|
+
/**
|
|
4293
|
+
* Starts fetching a model the app fetches itself (availability's
|
|
4294
|
+
* `download`), which the viewer asked for: show its size first. Not part
|
|
4295
|
+
* of FoundationModels, whose model the system manages.
|
|
4296
|
+
*/
|
|
4297
|
+
async download() {
|
|
4298
|
+
await perform({ action: "download" });
|
|
4299
|
+
this.changed();
|
|
4300
|
+
}
|
|
4301
|
+
/**
|
|
4302
|
+
* Makes the model with this `id` (one of availability's `models`) the one
|
|
4303
|
+
* that answers, from the next session on; remembered across launches.
|
|
4304
|
+
*/
|
|
4305
|
+
async selectModel(id) {
|
|
4306
|
+
await perform({ action: "select", id });
|
|
4307
|
+
this.changed();
|
|
4308
|
+
}
|
|
4309
|
+
/** Stops such a download, or removes the model from the device to free its space. */
|
|
4310
|
+
async deleteDownload() {
|
|
4311
|
+
await perform({ action: "deleteDownload" });
|
|
4312
|
+
this.changed();
|
|
4313
|
+
}
|
|
4314
|
+
/** @internal `useSystemLanguageModel` hears downloads start and models go. */
|
|
4315
|
+
listen(listener) {
|
|
4316
|
+
this.listeners.add(listener);
|
|
4317
|
+
return () => this.listeners.delete(listener);
|
|
4318
|
+
}
|
|
4319
|
+
changed() {
|
|
4320
|
+
for (const listener of this.listeners) listener();
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
function useSystemLanguageModel(model = SystemLanguageModel.default) {
|
|
4324
|
+
const [availability, setAvailability] = useState(null);
|
|
4325
|
+
useEffect(() => {
|
|
4326
|
+
let live = true;
|
|
4327
|
+
let timer;
|
|
4328
|
+
const check = () => {
|
|
4329
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4330
|
+
timer = void 0;
|
|
4331
|
+
void model.availability().then((next) => {
|
|
4332
|
+
if (!live) return;
|
|
4333
|
+
setAvailability((current) => JSON.stringify(current) === JSON.stringify(next) ? current : next);
|
|
4334
|
+
if (timer !== void 0) return;
|
|
4335
|
+
if (next.download?.progress !== void 0) timer = setTimeout(check, 1e3);
|
|
4336
|
+
else if (next.status === "unavailable" && next.reason === "modelNotReady" && !next.download) timer = setTimeout(check, 5e3);
|
|
4337
|
+
});
|
|
4338
|
+
};
|
|
4339
|
+
check();
|
|
4340
|
+
const stop = model.listen(check);
|
|
4341
|
+
return () => {
|
|
4342
|
+
live = false;
|
|
4343
|
+
stop();
|
|
4344
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4345
|
+
};
|
|
4346
|
+
}, [model]);
|
|
4347
|
+
return availability;
|
|
4348
|
+
}
|
|
4349
|
+
class ResponseStream {
|
|
4350
|
+
/** @internal Made by `LanguageModelSession.streamResponse`. */
|
|
4351
|
+
constructor(session, opened, prompt, options, ended) {
|
|
4352
|
+
this.ended = ended;
|
|
4353
|
+
this.started = opened.then(
|
|
4354
|
+
() => perform({ action: "stream", session: session.id, stream: this.id, prompt, ...options ? { options } : {} })
|
|
4355
|
+
);
|
|
4356
|
+
this.started.catch(() => this.end());
|
|
4357
|
+
}
|
|
4358
|
+
ended;
|
|
4359
|
+
id = nextId++;
|
|
4360
|
+
started;
|
|
4361
|
+
finished = false;
|
|
4362
|
+
last = { content: "" };
|
|
4363
|
+
async *[Symbol.asyncIterator]() {
|
|
4364
|
+
try {
|
|
4365
|
+
await this.started;
|
|
4366
|
+
while (!this.finished) {
|
|
4367
|
+
const { content, done } = await perform({ action: "next", stream: this.id });
|
|
4368
|
+
if (done) this.end();
|
|
4369
|
+
if (content !== this.last.content) {
|
|
4370
|
+
this.last = { content };
|
|
4371
|
+
yield this.last;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
} catch (error) {
|
|
4375
|
+
this.end();
|
|
4376
|
+
throw error;
|
|
4377
|
+
} finally {
|
|
4378
|
+
this.cancel();
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
/** The whole response, once it has ended. */
|
|
4382
|
+
async collect() {
|
|
4383
|
+
for await (const snapshot of this) this.last = snapshot;
|
|
4384
|
+
return this.last;
|
|
4385
|
+
}
|
|
4386
|
+
/** Stops the response; the iteration ends with what came so far. */
|
|
4387
|
+
cancel() {
|
|
4388
|
+
if (this.finished) return;
|
|
4389
|
+
this.end();
|
|
4390
|
+
void this.started.then(() => perform({ action: "cancel", stream: this.id })).catch(() => {
|
|
4391
|
+
});
|
|
4392
|
+
}
|
|
4393
|
+
end() {
|
|
4394
|
+
if (this.finished) return;
|
|
4395
|
+
this.finished = true;
|
|
4396
|
+
this.ended();
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
class LanguageModelSession {
|
|
4400
|
+
id = nextId++;
|
|
4401
|
+
instructions;
|
|
4402
|
+
tools;
|
|
4403
|
+
opened;
|
|
4404
|
+
responses = 0;
|
|
4405
|
+
closed = false;
|
|
4406
|
+
constructor(options = {}) {
|
|
4407
|
+
this.instructions = options.instructions;
|
|
4408
|
+
this.tools = options.tools ?? [];
|
|
4409
|
+
const tools = this.tools.map(({ name, description, parameters }) => ({ name, description, parameters }));
|
|
4410
|
+
this.opened = perform({
|
|
4411
|
+
action: "open",
|
|
4412
|
+
session: this.id,
|
|
4413
|
+
...options.instructions ? { instructions: options.instructions } : {},
|
|
4414
|
+
...tools.length > 0 ? { tools } : {}
|
|
4415
|
+
});
|
|
4416
|
+
this.opened.catch(() => {
|
|
4417
|
+
});
|
|
4418
|
+
if (tools.length > 0) void this.opened.then(() => this.serveTools()).catch(() => {
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
/** Runs the model's tool calls as they come, until the session closes. */
|
|
4422
|
+
async serveTools() {
|
|
4423
|
+
while (!this.closed) {
|
|
4424
|
+
const request = await perform({ action: "toolCall", session: this.id });
|
|
4425
|
+
if (!request || request.closed || request.call === void 0) return;
|
|
4426
|
+
const tool = this.tools.find((candidate) => candidate.name === request.name);
|
|
4427
|
+
let answer;
|
|
4428
|
+
try {
|
|
4429
|
+
if (!tool) throw new Error(`no tool named ${String(request.name)}`);
|
|
4430
|
+
const output = await tool.call(request.arguments ?? {});
|
|
4431
|
+
answer = { output: typeof output === "string" ? output : JSON.stringify(output ?? null) };
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
answer = { error: error instanceof Error ? error.message : String(error) };
|
|
4434
|
+
}
|
|
4435
|
+
await perform({ action: "toolResult", session: this.id, call: request.call, ...answer });
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
/** Whether a response is under way. */
|
|
4439
|
+
get isResponding() {
|
|
4440
|
+
return this.responses > 0;
|
|
4441
|
+
}
|
|
4442
|
+
/** Loads the model ahead of the first prompt, so it answers sooner. */
|
|
4443
|
+
prewarm() {
|
|
4444
|
+
void this.opened.then(() => perform({ action: "prewarm", session: this.id })).catch(() => {
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
async respond(to, options) {
|
|
4448
|
+
this.responses += 1;
|
|
4449
|
+
try {
|
|
4450
|
+
await this.opened;
|
|
4451
|
+
return await perform({ action: "respond", session: this.id, prompt: to, ...options ? { options } : {} });
|
|
4452
|
+
} finally {
|
|
4453
|
+
this.responses -= 1;
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
streamResponse(to, options) {
|
|
4457
|
+
this.responses += 1;
|
|
4458
|
+
return new ResponseStream(this, this.opened, to, options, () => {
|
|
4459
|
+
this.responses -= 1;
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
/** Ends the session: the host lets its conversation go. */
|
|
4463
|
+
close() {
|
|
4464
|
+
if (this.closed) return;
|
|
4465
|
+
this.closed = true;
|
|
4466
|
+
void this.opened.then(() => perform({ action: "close", session: this.id })).catch(() => {
|
|
4467
|
+
});
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
3577
4470
|
function wakeLockAPI() {
|
|
3578
4471
|
const nav = globalThis.navigator;
|
|
3579
4472
|
return nav?.wakeLock ?? null;
|
|
@@ -4321,13 +5214,11 @@ const NONE = {
|
|
|
4321
5214
|
caches: "file:///data/Library/Caches",
|
|
4322
5215
|
temporary: "file:///tmp"
|
|
4323
5216
|
};
|
|
4324
|
-
|
|
5217
|
+
function hostData(contents) {
|
|
4325
5218
|
if (typeof contents === "string") return new TextEncoder().encode(contents);
|
|
4326
5219
|
if (contents instanceof Uint8Array) return contents;
|
|
4327
5220
|
if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
|
|
4328
|
-
|
|
4329
|
-
if (typeof blob.bytes === "function") return await blob.bytes();
|
|
4330
|
-
return new Uint8Array(await blob.arrayBuffer());
|
|
5221
|
+
return contents;
|
|
4331
5222
|
}
|
|
4332
5223
|
function createFileManager(host) {
|
|
4333
5224
|
const required = () => {
|
|
@@ -4353,9 +5244,16 @@ function createFileManager(host) {
|
|
|
4353
5244
|
return directories().temporary;
|
|
4354
5245
|
},
|
|
4355
5246
|
url: (directory, path) => fileURL(directory, path),
|
|
4356
|
-
contents: async (url) =>
|
|
5247
|
+
contents: async (url, range) => {
|
|
5248
|
+
const files = required();
|
|
5249
|
+
if (!range) return files.read(url);
|
|
5250
|
+
if (files.readRange) return files.readRange(url, range.start, range.end);
|
|
5251
|
+
const all = await files.read(url);
|
|
5252
|
+
const start = Math.min(Math.max(0, range.start), all.byteLength);
|
|
5253
|
+
return all.subarray(start, Math.min(Math.max(start, range.end), all.byteLength));
|
|
5254
|
+
},
|
|
4357
5255
|
text: async (url) => new TextDecoder().decode(await required().read(url)),
|
|
4358
|
-
write: async (url, contents) => required().write(url,
|
|
5256
|
+
write: async (url, contents) => required().write(url, hostData(contents)),
|
|
4359
5257
|
contentsOfDirectory: async (url) => required().list(url),
|
|
4360
5258
|
createDirectory: async (url) => required().createDirectory(url),
|
|
4361
5259
|
fileExists: async (url) => await required().attributes(url) !== null,
|
|
@@ -4462,6 +5360,8 @@ function List(props) {
|
|
|
4462
5360
|
if (editing) listProps["editing"] = true;
|
|
4463
5361
|
let selection = null;
|
|
4464
5362
|
const split = useContext(SplitSelectionContext);
|
|
5363
|
+
let reportNow = () => {
|
|
5364
|
+
};
|
|
4465
5365
|
let selected = false;
|
|
4466
5366
|
let clearSelection = NO_CLEAR;
|
|
4467
5367
|
if (own.selection !== void 0) {
|
|
@@ -4478,13 +5378,16 @@ function List(props) {
|
|
|
4478
5378
|
if (next.has(tag)) next.delete(tag);
|
|
4479
5379
|
else next.add(tag);
|
|
4480
5380
|
setValue(next);
|
|
5381
|
+
reportNow(next.size > 0, () => setValue(/* @__PURE__ */ new Set()));
|
|
4481
5382
|
} else {
|
|
4482
|
-
|
|
5383
|
+
const next = editing && Object.is(value, tag) ? null : tag;
|
|
5384
|
+
setValue(next);
|
|
5385
|
+
reportNow(next !== null, () => setValue(null));
|
|
4483
5386
|
}
|
|
4484
5387
|
}
|
|
4485
5388
|
};
|
|
4486
5389
|
}
|
|
4487
|
-
useReportSplitSelection(split, selected, clearSelection, own.selection !== void 0);
|
|
5390
|
+
reportNow = useReportSplitSelection(split, selected, clearSelection, own.selection !== void 0);
|
|
4488
5391
|
const refresh = useRefreshable(takeModifier(modifiers, "refreshable"));
|
|
4489
5392
|
Object.assign(listProps, refresh.props);
|
|
4490
5393
|
const ownSettings = takeScrollSettings(modifiers);
|
|
@@ -4502,15 +5405,26 @@ function Form(props) {
|
|
|
4502
5405
|
function Section(props) {
|
|
4503
5406
|
const { own, modifiers } = splitProps(props);
|
|
4504
5407
|
const pinned = useContext(PinnedContext);
|
|
5408
|
+
const [expanded, setExpanded] = own.isExpanded !== void 0 ? readBinding(own.isExpanded, own.onExpandedChange) : [true, () => {
|
|
5409
|
+
}];
|
|
5410
|
+
const collapsible = own.isExpanded !== void 0;
|
|
4505
5411
|
let header = supplement(own.header);
|
|
4506
5412
|
let footer = supplement(own.footer);
|
|
4507
5413
|
if (pinned?.includes("sectionHeaders") && header.text !== null && !header.view) header = { text: null, view: /* @__PURE__ */ jsx(Text, { children: header.text }) };
|
|
4508
5414
|
if (pinned?.includes("sectionFooters") && footer.text !== null && !footer.view) footer = { text: null, view: /* @__PURE__ */ jsx(Text, { children: footer.text }) };
|
|
4509
5415
|
const children = [];
|
|
4510
|
-
if (header.view) children.push(sb("sectionheader", {}, void 0, header.view));
|
|
4511
|
-
if (own.children !== void 0) children.push(wrapRows(own.children));
|
|
4512
|
-
if (footer.view) children.push(sb("sectionfooter", {}, void 0, footer.view));
|
|
4513
|
-
return applyModifiers(
|
|
5416
|
+
if (header.view) children.push(/* @__PURE__ */ jsx(Fragment, { children: sb("sectionheader", {}, void 0, header.view) }, "header"));
|
|
5417
|
+
if (own.children !== void 0) children.push(/* @__PURE__ */ jsx(Fragment, { children: wrapRows(own.children) }, "rows"));
|
|
5418
|
+
if (footer.view) children.push(/* @__PURE__ */ jsx(Fragment, { children: sb("sectionfooter", {}, void 0, footer.view) }, "footer"));
|
|
5419
|
+
return applyModifiers(
|
|
5420
|
+
sb(
|
|
5421
|
+
"section",
|
|
5422
|
+
collapsible ? { header: header.text, footer: footer.text, collapsible: true, expanded } : { header: header.text, footer: footer.text },
|
|
5423
|
+
collapsible ? { tap: () => setExpanded(!expanded) } : void 0,
|
|
5424
|
+
children
|
|
5425
|
+
),
|
|
5426
|
+
modifiers
|
|
5427
|
+
);
|
|
4514
5428
|
}
|
|
4515
5429
|
function wrapRows(children) {
|
|
4516
5430
|
let offset = 0;
|
|
@@ -4618,6 +5532,11 @@ function Host(props) {
|
|
|
4618
5532
|
);
|
|
4619
5533
|
return applyModifiers(sb("host", { content: key, sizing }), modifiers);
|
|
4620
5534
|
}
|
|
5535
|
+
function RegisteredView(all) {
|
|
5536
|
+
const { own, modifiers } = splitProps(all);
|
|
5537
|
+
const on = own.onEvent ? { registered: (event) => own.onEvent(event.value) } : void 0;
|
|
5538
|
+
return applyModifiers(sb("registered", { name: own.name, props: own.props ?? {} }, on, own.children), modifiers);
|
|
5539
|
+
}
|
|
4621
5540
|
function Menu(props) {
|
|
4622
5541
|
const { own, modifiers } = splitProps(props);
|
|
4623
5542
|
const label = own.title !== void 0 ? /* @__PURE__ */ jsx(Text, { children: own.title }) : own.label;
|
|
@@ -5182,22 +6101,10 @@ function DisclosureGroup(props) {
|
|
|
5182
6101
|
const label = typeof own.label === "string" ? /* @__PURE__ */ jsx(Text, { children: own.label }) : own.label;
|
|
5183
6102
|
const toggle = () => withAnimation({ type: "easeInOut", duration: 0.25 }, () => setExpanded(!expanded));
|
|
5184
6103
|
return applyModifiers(
|
|
5185
|
-
|
|
5186
|
-
/* @__PURE__ */ jsx(
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
/* @__PURE__ */ jsx(
|
|
5190
|
-
Image$1,
|
|
5191
|
-
{
|
|
5192
|
-
systemName: "chevron.right",
|
|
5193
|
-
font: { textStyle: "footnote", weight: "semibold" },
|
|
5194
|
-
foregroundStyle: "tint",
|
|
5195
|
-
rotationEffect: expanded ? 90 : 0
|
|
5196
|
-
}
|
|
5197
|
-
)
|
|
5198
|
-
] }) }),
|
|
5199
|
-
expanded && /* @__PURE__ */ jsx(VStack, { alignment: "leading", spacing: 8, padding: ["top", 8], transition: { kind: "opacity" }, children: own.children })
|
|
5200
|
-
] }),
|
|
6104
|
+
sb("disclosuregroup", { expanded }, { tap: toggle }, [
|
|
6105
|
+
/* @__PURE__ */ jsx(Fragment, { children: sb("zstack", { alignment: "center" }, void 0, label) }, "label"),
|
|
6106
|
+
/* @__PURE__ */ jsx(Fragment, { children: own.children }, "content")
|
|
6107
|
+
]),
|
|
5201
6108
|
modifiers
|
|
5202
6109
|
);
|
|
5203
6110
|
}
|
|
@@ -5293,16 +6200,46 @@ function ColorPicker(props) {
|
|
|
5293
6200
|
modifiers
|
|
5294
6201
|
);
|
|
5295
6202
|
}
|
|
6203
|
+
const isFile = (item) => typeof item === "object" && !(item instanceof URL) && "data" in item;
|
|
6204
|
+
const MIME_BY_EXTENSION = {
|
|
6205
|
+
txt: "text/plain",
|
|
6206
|
+
md: "text/markdown",
|
|
6207
|
+
csv: "text/csv",
|
|
6208
|
+
json: "application/json",
|
|
6209
|
+
html: "text/html",
|
|
6210
|
+
pdf: "application/pdf",
|
|
6211
|
+
png: "image/png",
|
|
6212
|
+
jpg: "image/jpeg",
|
|
6213
|
+
jpeg: "image/jpeg",
|
|
6214
|
+
gif: "image/gif",
|
|
6215
|
+
svg: "image/svg+xml",
|
|
6216
|
+
m4a: "audio/mp4",
|
|
6217
|
+
mp3: "audio/mpeg",
|
|
6218
|
+
mp4: "video/mp4",
|
|
6219
|
+
mov: "video/quicktime",
|
|
6220
|
+
zip: "application/zip"
|
|
6221
|
+
};
|
|
6222
|
+
async function sharedFile(file) {
|
|
6223
|
+
const bytes = typeof file.data === "string" ? new TextEncoder().encode(file.data) : file.data instanceof Uint8Array ? file.data : file.data instanceof ArrayBuffer ? new Uint8Array(file.data) : new Uint8Array(await file.data.arrayBuffer());
|
|
6224
|
+
const extension = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".") + 1).toLowerCase() : "";
|
|
6225
|
+
return { name: file.name, type: file.type ?? MIME_BY_EXTENSION[extension] ?? "application/octet-stream", data: base64Encode(bytes) };
|
|
6226
|
+
}
|
|
6227
|
+
async function shareParamsOf(props) {
|
|
6228
|
+
const item = props.item;
|
|
6229
|
+
const isURL = item instanceof URL || typeof item === "string" && /^[a-z][a-z0-9+.-]*:\/\//i.test(item);
|
|
6230
|
+
const params = {};
|
|
6231
|
+
if (props.subject) params.title = props.subject;
|
|
6232
|
+
if (isURL) params.url = String(item);
|
|
6233
|
+
const text = [props.message, typeof item === "string" && !isURL ? item : null].filter(Boolean).join("\n");
|
|
6234
|
+
if (text) params.text = text;
|
|
6235
|
+
const files = [...isFile(item) ? [item] : [], ...props.files ?? []];
|
|
6236
|
+
if (files.length) params.files = await Promise.all(files.map(sharedFile));
|
|
6237
|
+
return params;
|
|
6238
|
+
}
|
|
5296
6239
|
function ShareLink(props) {
|
|
5297
6240
|
const { own, modifiers } = splitProps(props);
|
|
5298
6241
|
const share = () => {
|
|
5299
|
-
|
|
5300
|
-
const params = {};
|
|
5301
|
-
if (own.subject) params["title"] = own.subject;
|
|
5302
|
-
if (isURL) params["url"] = String(own.item);
|
|
5303
|
-
const text = [own.message, isURL ? null : String(own.item)].filter(Boolean).join("\n");
|
|
5304
|
-
if (text) params["text"] = text;
|
|
5305
|
-
performHostRequest({ op: "request", id: 0, kind: "share", params }, browserEnvironment).catch((error) => console.error(error));
|
|
6242
|
+
shareParamsOf(own).then((params) => performHostRequest({ op: "request", id: 0, kind: "share", params }, browserEnvironment)).catch((error) => console.error(error));
|
|
5306
6243
|
};
|
|
5307
6244
|
return applyModifiers(
|
|
5308
6245
|
/* @__PURE__ */ jsx(Button, { action: share, children: own.children ?? /* @__PURE__ */ jsx(Label, { title: own.title ?? "Share", systemImage: "square.and.arrow.up" }) }),
|
|
@@ -5923,6 +6860,23 @@ function KeyframeAnimator({
|
|
|
5923
6860
|
return children(value);
|
|
5924
6861
|
}
|
|
5925
6862
|
export {
|
|
6863
|
+
AVAudioEngine,
|
|
6864
|
+
AVAudioFile,
|
|
6865
|
+
AVAudioInputNode,
|
|
6866
|
+
AVAudioMixerNode,
|
|
6867
|
+
AVAudioNode,
|
|
6868
|
+
AVAudioOutputNode,
|
|
6869
|
+
AVAudioPCMBuffer,
|
|
6870
|
+
AVAudioPlayerNode,
|
|
6871
|
+
AVAudioTime,
|
|
6872
|
+
AVAudioUnitDelay,
|
|
6873
|
+
AVAudioUnitDistortion,
|
|
6874
|
+
AVAudioUnitEQ,
|
|
6875
|
+
AVAudioUnitEQFilterParameters,
|
|
6876
|
+
AVAudioUnitEffect,
|
|
6877
|
+
AVAudioUnitReverb,
|
|
6878
|
+
AVAudioUnitSampler,
|
|
6879
|
+
AVAudioUnitTimePitch,
|
|
5926
6880
|
AVPlayer,
|
|
5927
6881
|
AVQueuePlayer,
|
|
5928
6882
|
AngularGradient,
|
|
@@ -5953,6 +6907,7 @@ export {
|
|
|
5953
6907
|
ForEach,
|
|
5954
6908
|
Form,
|
|
5955
6909
|
Gauge,
|
|
6910
|
+
GenerationError,
|
|
5956
6911
|
GeometryChangeView,
|
|
5957
6912
|
GeometryReader,
|
|
5958
6913
|
GraphicsContext,
|
|
@@ -5968,6 +6923,7 @@ export {
|
|
|
5968
6923
|
LOADING_INDICATOR_DELAY_MS,
|
|
5969
6924
|
Label,
|
|
5970
6925
|
LabeledContent,
|
|
6926
|
+
LanguageModelSession,
|
|
5971
6927
|
Layout,
|
|
5972
6928
|
LazyHGrid,
|
|
5973
6929
|
LazyHStack,
|
|
@@ -5991,6 +6947,8 @@ export {
|
|
|
5991
6947
|
ProgressView,
|
|
5992
6948
|
RadialGradient,
|
|
5993
6949
|
Rectangle,
|
|
6950
|
+
RegisteredView,
|
|
6951
|
+
ResponseStream,
|
|
5994
6952
|
RoundedRectangle,
|
|
5995
6953
|
Row,
|
|
5996
6954
|
RuleMark,
|
|
@@ -6006,6 +6964,7 @@ export {
|
|
|
6006
6964
|
SplitSelectionContext,
|
|
6007
6965
|
Stepper,
|
|
6008
6966
|
SwiftUIView,
|
|
6967
|
+
SystemLanguageModel,
|
|
6009
6968
|
Tab,
|
|
6010
6969
|
TabView,
|
|
6011
6970
|
Table,
|
|
@@ -6022,6 +6981,7 @@ export {
|
|
|
6022
6981
|
UnevenRoundedRectangle,
|
|
6023
6982
|
VStack,
|
|
6024
6983
|
VideoPlayer,
|
|
6984
|
+
ViewRegistry,
|
|
6025
6985
|
ViewThatFits,
|
|
6026
6986
|
VisualEffect,
|
|
6027
6987
|
ZStack,
|
|
@@ -6052,6 +7012,7 @@ export {
|
|
|
6052
7012
|
moveItems,
|
|
6053
7013
|
nextGeometry,
|
|
6054
7014
|
niceStep,
|
|
7015
|
+
noteInName,
|
|
6055
7016
|
opacity,
|
|
6056
7017
|
openURL,
|
|
6057
7018
|
padding,
|
|
@@ -6060,8 +7021,10 @@ export {
|
|
|
6060
7021
|
providingScrollSettings,
|
|
6061
7022
|
readBinding,
|
|
6062
7023
|
rgb,
|
|
7024
|
+
samplerZones,
|
|
6063
7025
|
scrollGeometryOf,
|
|
6064
7026
|
shapeStyle,
|
|
7027
|
+
shareParamsOf,
|
|
6065
7028
|
signInWithApple,
|
|
6066
7029
|
system,
|
|
6067
7030
|
takeScrollSettings,
|
|
@@ -6069,6 +7032,7 @@ export {
|
|
|
6069
7032
|
unitPoint,
|
|
6070
7033
|
useAVPlayer,
|
|
6071
7034
|
useAppStorage,
|
|
7035
|
+
useAudioEngine,
|
|
6072
7036
|
useAudioRecorder,
|
|
6073
7037
|
useColorScheme,
|
|
6074
7038
|
useContextBridge,
|
|
@@ -6086,6 +7050,7 @@ export {
|
|
|
6086
7050
|
useScenePhase,
|
|
6087
7051
|
useSpeechRecognition,
|
|
6088
7052
|
useSpeechSynthesis,
|
|
7053
|
+
useSystemLanguageModel,
|
|
6089
7054
|
useUndoManager,
|
|
6090
7055
|
useViewEnvironment,
|
|
6091
7056
|
useWakeLock,
|