@camstack/ui-library 1.2.174 → 1.2.176
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/composites/cap-settings/camera-grid-layout.d.ts +21 -1
- package/dist/generated/system-hooks.d.ts +4 -4
- package/dist/hooks/use-ptz.d.ts +12 -3
- package/dist/index.cjs +268 -191
- package/dist/index.js +267 -190
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6625,10 +6625,10 @@ var usePtzGetHomePreset = trpc.ptz.getHomePreset.useQuery;
|
|
|
6625
6625
|
var usePtzSetHomePreset = trpc.ptz.setHomePreset.useMutation;
|
|
6626
6626
|
/** Generated alias around `trpc.ptz.captureHomeHere.useMutation`. */
|
|
6627
6627
|
var usePtzCaptureHomeHere = trpc.ptz.captureHomeHere.useMutation;
|
|
6628
|
-
/** Generated alias around `trpc.ptz.
|
|
6629
|
-
var
|
|
6630
|
-
/** Generated alias around `trpc.ptz.
|
|
6631
|
-
var
|
|
6628
|
+
/** Generated alias around `trpc.ptz.getHomeReturn.useQuery`. */
|
|
6629
|
+
var usePtzGetHomeReturn = trpc.ptz.getHomeReturn.useQuery;
|
|
6630
|
+
/** Generated alias around `trpc.ptz.setHomeReturn.useMutation`. */
|
|
6631
|
+
var usePtzSetHomeReturn = trpc.ptz.setHomeReturn.useMutation;
|
|
6632
6632
|
/** Generated alias around `trpc.ptz.getPosition.useQuery`. */
|
|
6633
6633
|
var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
|
|
6634
6634
|
/** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
|
|
@@ -16261,6 +16261,136 @@ function AutotrackSection({ deviceId }) {
|
|
|
16261
16261
|
});
|
|
16262
16262
|
}
|
|
16263
16263
|
//#endregion
|
|
16264
|
+
//#region src/hooks/use-device-proxy.ts
|
|
16265
|
+
/**
|
|
16266
|
+
* useDeviceProxy — canonical React entry point for a device.
|
|
16267
|
+
*
|
|
16268
|
+
* Symmetric with the server's `ctx.fetchDevice(id)` and the SDK's
|
|
16269
|
+
* `BackendClient.fetchDevice(id)` — same `DeviceProxy` shape, same
|
|
16270
|
+
* routing semantics (cap-method calls go through the wrapper chain;
|
|
16271
|
+
* `state.<cap>.value` reads the kernel's mirrored snapshot, never
|
|
16272
|
+
* the provider).
|
|
16273
|
+
*
|
|
16274
|
+
* What you can do with the returned proxy:
|
|
16275
|
+
*
|
|
16276
|
+
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16277
|
+
*
|
|
16278
|
+
* // Reactive runtime state — re-renders when the slice changes.
|
|
16279
|
+
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16280
|
+
*
|
|
16281
|
+
* // Imperative cap-method calls — through the wrapper chain.
|
|
16282
|
+
* await dev?.snapshot?.getSnapshot({ streamId: 'high' })
|
|
16283
|
+
* await dev?.ptz?.move({ direction: 'left', steps: 1 })
|
|
16284
|
+
*
|
|
16285
|
+
* Lifecycle: the hook resolves bindings via `deviceManager.getBindings`
|
|
16286
|
+
* the first time `deviceId` becomes valid, and refreshes them on
|
|
16287
|
+
* `capability.binding-changed` events for that device. Returns `null`
|
|
16288
|
+
* until the first resolution completes (the proxy is not constructible
|
|
16289
|
+
* without a binding).
|
|
16290
|
+
*
|
|
16291
|
+
* Works in both admin-ui (`BackendClient.trpc`) and addon pages
|
|
16292
|
+
* (`AddonPageProps.trpc`) — parametrized by a minimal vanilla tRPC
|
|
16293
|
+
* proxy shape (matches both at runtime).
|
|
16294
|
+
*/
|
|
16295
|
+
var BINDING_CHANGED_CATEGORY = "capability.binding-changed";
|
|
16296
|
+
/**
|
|
16297
|
+
* @returns the proxy, or `null` while bindings are still loading or
|
|
16298
|
+
* when `deviceId` is null. Re-rendered when bindings change.
|
|
16299
|
+
*/
|
|
16300
|
+
function useDeviceProxy(trpc, deviceId) {
|
|
16301
|
+
const [binding, setBinding] = (0, react$1.useState)(null);
|
|
16302
|
+
(0, react$1.useEffect)(() => {
|
|
16303
|
+
if (deviceId === null) {
|
|
16304
|
+
setBinding(null);
|
|
16305
|
+
return;
|
|
16306
|
+
}
|
|
16307
|
+
let cancelled = false;
|
|
16308
|
+
const refresh = async () => {
|
|
16309
|
+
try {
|
|
16310
|
+
const next = await trpc.deviceManager.getBindings.query({ deviceId });
|
|
16311
|
+
if (!cancelled) setBinding(next);
|
|
16312
|
+
} catch {
|
|
16313
|
+
if (!cancelled) setBinding((prev) => prev);
|
|
16314
|
+
}
|
|
16315
|
+
};
|
|
16316
|
+
refresh();
|
|
16317
|
+
const sub = trpc.live?.onEvent.subscribe({ category: BINDING_CHANGED_CATEGORY }, { onData: (evt) => {
|
|
16318
|
+
const src = evt.data?.source;
|
|
16319
|
+
if (!src) {
|
|
16320
|
+
refresh();
|
|
16321
|
+
return;
|
|
16322
|
+
}
|
|
16323
|
+
if (src.type === "device" && src.id === deviceId) refresh();
|
|
16324
|
+
} });
|
|
16325
|
+
return () => {
|
|
16326
|
+
cancelled = true;
|
|
16327
|
+
sub?.unsubscribe();
|
|
16328
|
+
};
|
|
16329
|
+
}, [trpc, deviceId]);
|
|
16330
|
+
return (0, react$1.useMemo)(() => {
|
|
16331
|
+
if (!binding) return null;
|
|
16332
|
+
return (0, _camstack_types.createDeviceProxy)(trpc, binding);
|
|
16333
|
+
}, [trpc, binding]);
|
|
16334
|
+
}
|
|
16335
|
+
/**
|
|
16336
|
+
* Companion hook — reads a `SliceHandle` reactively, returning the
|
|
16337
|
+
* current value and re-rendering on every push from the kernel
|
|
16338
|
+
* mirror. Pass `dev?.state.battery` etc. as the handle.
|
|
16339
|
+
*
|
|
16340
|
+
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16341
|
+
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16342
|
+
*
|
|
16343
|
+
* Why a hook (and not just `dev.state.battery.subscribe(...)` inline)?
|
|
16344
|
+
* React doesn't observe external mutations on its own — a bare
|
|
16345
|
+
* subscribe call inside a render would fire on every render (memory
|
|
16346
|
+
* leak), wouldn't trigger a re-render on slice change, and wouldn't
|
|
16347
|
+
* clean up at unmount. The hook bridges the imperative
|
|
16348
|
+
* `(value, subscribe)` pair into React's declarative lifecycle via
|
|
16349
|
+
* `useSyncExternalStore` (the canonical React 18 primitive for
|
|
16350
|
+
* external stores — handles tearing in concurrent rendering and
|
|
16351
|
+
* de-dups identical pushes).
|
|
16352
|
+
*/
|
|
16353
|
+
function useDeviceStateSlice(handle) {
|
|
16354
|
+
const subscribe = (0, react$1.useCallback)((notify) => {
|
|
16355
|
+
if (!handle) return () => void 0;
|
|
16356
|
+
return handle.subscribe(() => notify());
|
|
16357
|
+
}, [handle]);
|
|
16358
|
+
const getSnapshot = (0, react$1.useCallback)(() => handle?.value, [handle]);
|
|
16359
|
+
return (0, react$1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
16360
|
+
}
|
|
16361
|
+
/**
|
|
16362
|
+
* One-line combinator — collapse the common
|
|
16363
|
+
* `useDeviceProxy + useDeviceStateSlice(dev?.state.<cap>)` pair into a
|
|
16364
|
+
* single hook. The selector receives the live `DeviceProxy['state']`
|
|
16365
|
+
* bag (typed against the codegen, so `s.battery` etc. autocomplete)
|
|
16366
|
+
* and returns one slice handle. Returns the slice value, undefined
|
|
16367
|
+
* while the binding resolves OR when the slice has no cached push yet.
|
|
16368
|
+
*
|
|
16369
|
+
* Idiomatic call-site:
|
|
16370
|
+
*
|
|
16371
|
+
* const battery = useDeviceState(trpc, deviceId, (s) => s.battery)
|
|
16372
|
+
* const zones = useDeviceState(trpc, deviceId, (s) => s.zones)
|
|
16373
|
+
*
|
|
16374
|
+
* vs. the manual pair:
|
|
16375
|
+
*
|
|
16376
|
+
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16377
|
+
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16378
|
+
* const zones = useDeviceStateSlice(dev?.state.zones)
|
|
16379
|
+
*
|
|
16380
|
+
* Use the manual pair when:
|
|
16381
|
+
* - you also need the proxy's cap method bag (`dev.zones?.addZone(...)`)
|
|
16382
|
+
* - you want to share the proxy across multiple slice reads with a
|
|
16383
|
+
* single binding fetch (the combinator calls useDeviceProxy
|
|
16384
|
+
* internally, which dedups bindings via React Query in practice
|
|
16385
|
+
* but spawns one extra subscription per call site)
|
|
16386
|
+
*
|
|
16387
|
+
* Use the combinator when reading just one slice — most common case.
|
|
16388
|
+
*/
|
|
16389
|
+
function useDeviceState(trpc, deviceId, select) {
|
|
16390
|
+
const dev = useDeviceProxy(trpc, deviceId);
|
|
16391
|
+
return useDeviceStateSlice(dev ? select(dev.state) : void 0);
|
|
16392
|
+
}
|
|
16393
|
+
//#endregion
|
|
16264
16394
|
//#region src/hooks/use-devices.ts
|
|
16265
16395
|
/**
|
|
16266
16396
|
* useDevices / useDevice — reactive sync-mirror reads.
|
|
@@ -16446,20 +16576,49 @@ function readCell(value) {
|
|
|
16446
16576
|
cell
|
|
16447
16577
|
};
|
|
16448
16578
|
}
|
|
16449
|
-
|
|
16579
|
+
function readProfileOffer(value) {
|
|
16580
|
+
if (!isRecord(value)) return null;
|
|
16581
|
+
const profile = value["profile"];
|
|
16582
|
+
const offered = value["offered"];
|
|
16583
|
+
const missing = value["missingSources"];
|
|
16584
|
+
if (profile !== "high" && profile !== "mid" && profile !== "low") return null;
|
|
16585
|
+
if (typeof offered !== "boolean" || !Array.isArray(missing)) return null;
|
|
16586
|
+
const missingSources = [];
|
|
16587
|
+
for (const entry of missing) {
|
|
16588
|
+
if (typeof entry !== "number" || !Number.isInteger(entry)) return null;
|
|
16589
|
+
missingSources.push(entry);
|
|
16590
|
+
}
|
|
16591
|
+
return {
|
|
16592
|
+
profile,
|
|
16593
|
+
offered,
|
|
16594
|
+
missingSources
|
|
16595
|
+
};
|
|
16596
|
+
}
|
|
16597
|
+
/**
|
|
16598
|
+
* Parse the `camera-grid-layout.getLayout` reply. See {@link GridLayoutReadOutcome}.
|
|
16599
|
+
*
|
|
16600
|
+
* The cap answers the view itself, or `null` — there is no envelope. It used to
|
|
16601
|
+
* come back as `{ grid }` from `addons.custom`, which was the mechanism the
|
|
16602
|
+
* widget reached the addon through before the geometry had a capability of its
|
|
16603
|
+
* own; the widget now asks THE CAMERA and the envelope went with the bridge.
|
|
16604
|
+
*/
|
|
16450
16605
|
function parseGridView(raw) {
|
|
16451
|
-
if (
|
|
16452
|
-
|
|
16453
|
-
|
|
16454
|
-
if (!
|
|
16455
|
-
const { instanceId, deviceId, name, width, height, fps, cells } = grid;
|
|
16456
|
-
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof width !== "number" || typeof height !== "number" || typeof fps !== "number" || !Array.isArray(cells)) return "unreadable";
|
|
16606
|
+
if (raw === null) return null;
|
|
16607
|
+
if (!isRecord(raw)) return "unreadable";
|
|
16608
|
+
const { instanceId, deviceId, name, width, height, fps, cells, profiles } = raw;
|
|
16609
|
+
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof width !== "number" || typeof height !== "number" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
16457
16610
|
const parsed = [];
|
|
16458
16611
|
for (const entry of cells) {
|
|
16459
16612
|
const cell = readCell(entry);
|
|
16460
16613
|
if (cell === null) return "unreadable";
|
|
16461
16614
|
parsed.push(cell);
|
|
16462
16615
|
}
|
|
16616
|
+
const offers = [];
|
|
16617
|
+
for (const entry of profiles) {
|
|
16618
|
+
const offer = readProfileOffer(entry);
|
|
16619
|
+
if (offer === null) return "unreadable";
|
|
16620
|
+
offers.push(offer);
|
|
16621
|
+
}
|
|
16463
16622
|
return {
|
|
16464
16623
|
instanceId,
|
|
16465
16624
|
deviceId,
|
|
@@ -16467,7 +16626,8 @@ function parseGridView(raw) {
|
|
|
16467
16626
|
width,
|
|
16468
16627
|
height,
|
|
16469
16628
|
fps,
|
|
16470
|
-
cells: parsed
|
|
16629
|
+
cells: parsed,
|
|
16630
|
+
profiles: offers
|
|
16471
16631
|
};
|
|
16472
16632
|
}
|
|
16473
16633
|
/**
|
|
@@ -16525,7 +16685,6 @@ function resizeNormalizedRect(rect, dw, dh) {
|
|
|
16525
16685
|
* the server nothing and it is wrong in exactly the ways the real composition
|
|
16526
16686
|
* would be wrong.
|
|
16527
16687
|
*/
|
|
16528
|
-
var ADDON_ID = "camera-grid";
|
|
16529
16688
|
var FULL_FRAME = {
|
|
16530
16689
|
x: 0,
|
|
16531
16690
|
y: 0,
|
|
@@ -16544,36 +16703,36 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16544
16703
|
const [saveError, setSaveError] = (0, react$1.useState)(null);
|
|
16545
16704
|
const [selected, setSelected] = (0, react$1.useState)(0);
|
|
16546
16705
|
const [drag, setDrag] = (0, react$1.useState)(null);
|
|
16706
|
+
const dev = useDeviceProxy(system.trpcClient, deviceId);
|
|
16547
16707
|
const reload = (0, react$1.useCallback)(async () => {
|
|
16548
|
-
const
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
input: { deviceId }
|
|
16552
|
-
}));
|
|
16708
|
+
const layout = dev?.cameraGridLayout;
|
|
16709
|
+
if (!layout) return;
|
|
16710
|
+
const outcome = parseGridView(await layout.getLayout({}));
|
|
16553
16711
|
setLoaded(outcome);
|
|
16554
16712
|
setDraft(outcome === null || outcome === "unreadable" ? null : outcome);
|
|
16555
|
-
}, [
|
|
16713
|
+
}, [dev]);
|
|
16556
16714
|
(0, react$1.useEffect)(() => {
|
|
16715
|
+
if (!dev) return;
|
|
16716
|
+
if (!dev.cameraGridLayout) {
|
|
16717
|
+
setLoaded(null);
|
|
16718
|
+
return;
|
|
16719
|
+
}
|
|
16557
16720
|
reload().catch(() => {
|
|
16558
16721
|
setLoaded("unreadable");
|
|
16559
16722
|
});
|
|
16560
|
-
}, [reload]);
|
|
16723
|
+
}, [dev, reload]);
|
|
16561
16724
|
const save = (0, react$1.useCallback)(async () => {
|
|
16562
|
-
|
|
16725
|
+
const layout = dev?.cameraGridLayout;
|
|
16726
|
+
if (draft === null || !layout) return;
|
|
16563
16727
|
setSaving(true);
|
|
16564
16728
|
setSaveError(null);
|
|
16565
16729
|
try {
|
|
16566
|
-
const outcome = parseGridView(await
|
|
16567
|
-
|
|
16568
|
-
|
|
16569
|
-
|
|
16570
|
-
|
|
16571
|
-
|
|
16572
|
-
width: draft.width,
|
|
16573
|
-
height: draft.height,
|
|
16574
|
-
fps: draft.fps,
|
|
16575
|
-
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16576
|
-
}
|
|
16730
|
+
const outcome = parseGridView(await layout.saveLayout({
|
|
16731
|
+
name: draft.name,
|
|
16732
|
+
width: draft.width,
|
|
16733
|
+
height: draft.height,
|
|
16734
|
+
fps: draft.fps,
|
|
16735
|
+
cells: draft.cells.map((cell) => ({ ...cell }))
|
|
16577
16736
|
}));
|
|
16578
16737
|
if (outcome !== null && outcome !== "unreadable") {
|
|
16579
16738
|
setLoaded(outcome);
|
|
@@ -16584,11 +16743,7 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16584
16743
|
} finally {
|
|
16585
16744
|
setSaving(false);
|
|
16586
16745
|
}
|
|
16587
|
-
}, [
|
|
16588
|
-
system,
|
|
16589
|
-
deviceId,
|
|
16590
|
-
draft
|
|
16591
|
-
]);
|
|
16746
|
+
}, [dev, draft]);
|
|
16592
16747
|
const patchCell = (0, react$1.useCallback)((index, patch) => {
|
|
16593
16748
|
setDraft((prev) => prev === null ? prev : {
|
|
16594
16749
|
...prev,
|
|
@@ -16742,6 +16897,10 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16742
16897
|
})
|
|
16743
16898
|
]
|
|
16744
16899
|
}),
|
|
16900
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GridProfileReport, {
|
|
16901
|
+
offers: loaded.profiles,
|
|
16902
|
+
cameras
|
|
16903
|
+
}),
|
|
16745
16904
|
draft.cells[selected] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CellInspector, {
|
|
16746
16905
|
cell: draft.cells[selected],
|
|
16747
16906
|
index: selected,
|
|
@@ -16761,6 +16920,37 @@ function CameraGridLayoutEditor({ deviceId }) {
|
|
|
16761
16920
|
})
|
|
16762
16921
|
});
|
|
16763
16922
|
}
|
|
16923
|
+
/**
|
|
16924
|
+
* The honest report. A grid's `high` composes its sources' `high` and its `low`
|
|
16925
|
+
* their `low`, so a profile is on offer only when EVERY source can serve it —
|
|
16926
|
+
* and the refusal names the source, because "this grid has no low" is not
|
|
16927
|
+
* something an operator can act on and "615 has no low" is.
|
|
16928
|
+
*
|
|
16929
|
+
* Read-only on purpose: what is on offer is decided by the source cameras'
|
|
16930
|
+
* assignments, and a control here would be a switch that writes nothing (D62).
|
|
16931
|
+
*/
|
|
16932
|
+
function GridProfileReport({ offers, cameras }) {
|
|
16933
|
+
const nameOf = (deviceId) => cameras.find((camera) => camera.id === deviceId)?.name ?? `#${String(deviceId)}`;
|
|
16934
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16935
|
+
className: "flex flex-col gap-1 rounded border border-border p-2 text-[10.5px]",
|
|
16936
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16937
|
+
className: "uppercase text-foreground-subtle",
|
|
16938
|
+
children: "Published profiles"
|
|
16939
|
+
}), offers.map((offer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16940
|
+
className: "flex items-baseline gap-2",
|
|
16941
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16942
|
+
className: "w-10 uppercase",
|
|
16943
|
+
children: offer.profile
|
|
16944
|
+
}), offer.offered ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
16945
|
+
className: "text-foreground-subtle",
|
|
16946
|
+
children: ["composed from every source’s ", offer.profile]
|
|
16947
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16948
|
+
className: "text-danger",
|
|
16949
|
+
children: offer.missingSources.length === 0 ? "not offered — this grid has no cells yet" : `not offered — no ${offer.profile} on ${offer.missingSources.map(nameOf).join(", ")}`
|
|
16950
|
+
})]
|
|
16951
|
+
}, offer.profile))]
|
|
16952
|
+
});
|
|
16953
|
+
}
|
|
16764
16954
|
function GridCellBox({ cell, selected, onSelect, onDragStart }) {
|
|
16765
16955
|
const { src } = useDeviceSnapshotImage(cell.deviceId, { width: 640 });
|
|
16766
16956
|
const backgroundSize = `${String(100 / cell.source.width)}% ${String(100 / cell.source.height)}%`;
|
|
@@ -16866,136 +17056,6 @@ function LabelledNumber({ label, value, step = 1, onChange }) {
|
|
|
16866
17056
|
});
|
|
16867
17057
|
}
|
|
16868
17058
|
//#endregion
|
|
16869
|
-
//#region src/hooks/use-device-proxy.ts
|
|
16870
|
-
/**
|
|
16871
|
-
* useDeviceProxy — canonical React entry point for a device.
|
|
16872
|
-
*
|
|
16873
|
-
* Symmetric with the server's `ctx.fetchDevice(id)` and the SDK's
|
|
16874
|
-
* `BackendClient.fetchDevice(id)` — same `DeviceProxy` shape, same
|
|
16875
|
-
* routing semantics (cap-method calls go through the wrapper chain;
|
|
16876
|
-
* `state.<cap>.value` reads the kernel's mirrored snapshot, never
|
|
16877
|
-
* the provider).
|
|
16878
|
-
*
|
|
16879
|
-
* What you can do with the returned proxy:
|
|
16880
|
-
*
|
|
16881
|
-
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16882
|
-
*
|
|
16883
|
-
* // Reactive runtime state — re-renders when the slice changes.
|
|
16884
|
-
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16885
|
-
*
|
|
16886
|
-
* // Imperative cap-method calls — through the wrapper chain.
|
|
16887
|
-
* await dev?.snapshot?.getSnapshot({ streamId: 'high' })
|
|
16888
|
-
* await dev?.ptz?.move({ direction: 'left', steps: 1 })
|
|
16889
|
-
*
|
|
16890
|
-
* Lifecycle: the hook resolves bindings via `deviceManager.getBindings`
|
|
16891
|
-
* the first time `deviceId` becomes valid, and refreshes them on
|
|
16892
|
-
* `capability.binding-changed` events for that device. Returns `null`
|
|
16893
|
-
* until the first resolution completes (the proxy is not constructible
|
|
16894
|
-
* without a binding).
|
|
16895
|
-
*
|
|
16896
|
-
* Works in both admin-ui (`BackendClient.trpc`) and addon pages
|
|
16897
|
-
* (`AddonPageProps.trpc`) — parametrized by a minimal vanilla tRPC
|
|
16898
|
-
* proxy shape (matches both at runtime).
|
|
16899
|
-
*/
|
|
16900
|
-
var BINDING_CHANGED_CATEGORY = "capability.binding-changed";
|
|
16901
|
-
/**
|
|
16902
|
-
* @returns the proxy, or `null` while bindings are still loading or
|
|
16903
|
-
* when `deviceId` is null. Re-rendered when bindings change.
|
|
16904
|
-
*/
|
|
16905
|
-
function useDeviceProxy(trpc, deviceId) {
|
|
16906
|
-
const [binding, setBinding] = (0, react$1.useState)(null);
|
|
16907
|
-
(0, react$1.useEffect)(() => {
|
|
16908
|
-
if (deviceId === null) {
|
|
16909
|
-
setBinding(null);
|
|
16910
|
-
return;
|
|
16911
|
-
}
|
|
16912
|
-
let cancelled = false;
|
|
16913
|
-
const refresh = async () => {
|
|
16914
|
-
try {
|
|
16915
|
-
const next = await trpc.deviceManager.getBindings.query({ deviceId });
|
|
16916
|
-
if (!cancelled) setBinding(next);
|
|
16917
|
-
} catch {
|
|
16918
|
-
if (!cancelled) setBinding((prev) => prev);
|
|
16919
|
-
}
|
|
16920
|
-
};
|
|
16921
|
-
refresh();
|
|
16922
|
-
const sub = trpc.live?.onEvent.subscribe({ category: BINDING_CHANGED_CATEGORY }, { onData: (evt) => {
|
|
16923
|
-
const src = evt.data?.source;
|
|
16924
|
-
if (!src) {
|
|
16925
|
-
refresh();
|
|
16926
|
-
return;
|
|
16927
|
-
}
|
|
16928
|
-
if (src.type === "device" && src.id === deviceId) refresh();
|
|
16929
|
-
} });
|
|
16930
|
-
return () => {
|
|
16931
|
-
cancelled = true;
|
|
16932
|
-
sub?.unsubscribe();
|
|
16933
|
-
};
|
|
16934
|
-
}, [trpc, deviceId]);
|
|
16935
|
-
return (0, react$1.useMemo)(() => {
|
|
16936
|
-
if (!binding) return null;
|
|
16937
|
-
return (0, _camstack_types.createDeviceProxy)(trpc, binding);
|
|
16938
|
-
}, [trpc, binding]);
|
|
16939
|
-
}
|
|
16940
|
-
/**
|
|
16941
|
-
* Companion hook — reads a `SliceHandle` reactively, returning the
|
|
16942
|
-
* current value and re-rendering on every push from the kernel
|
|
16943
|
-
* mirror. Pass `dev?.state.battery` etc. as the handle.
|
|
16944
|
-
*
|
|
16945
|
-
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16946
|
-
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16947
|
-
*
|
|
16948
|
-
* Why a hook (and not just `dev.state.battery.subscribe(...)` inline)?
|
|
16949
|
-
* React doesn't observe external mutations on its own — a bare
|
|
16950
|
-
* subscribe call inside a render would fire on every render (memory
|
|
16951
|
-
* leak), wouldn't trigger a re-render on slice change, and wouldn't
|
|
16952
|
-
* clean up at unmount. The hook bridges the imperative
|
|
16953
|
-
* `(value, subscribe)` pair into React's declarative lifecycle via
|
|
16954
|
-
* `useSyncExternalStore` (the canonical React 18 primitive for
|
|
16955
|
-
* external stores — handles tearing in concurrent rendering and
|
|
16956
|
-
* de-dups identical pushes).
|
|
16957
|
-
*/
|
|
16958
|
-
function useDeviceStateSlice(handle) {
|
|
16959
|
-
const subscribe = (0, react$1.useCallback)((notify) => {
|
|
16960
|
-
if (!handle) return () => void 0;
|
|
16961
|
-
return handle.subscribe(() => notify());
|
|
16962
|
-
}, [handle]);
|
|
16963
|
-
const getSnapshot = (0, react$1.useCallback)(() => handle?.value, [handle]);
|
|
16964
|
-
return (0, react$1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
16965
|
-
}
|
|
16966
|
-
/**
|
|
16967
|
-
* One-line combinator — collapse the common
|
|
16968
|
-
* `useDeviceProxy + useDeviceStateSlice(dev?.state.<cap>)` pair into a
|
|
16969
|
-
* single hook. The selector receives the live `DeviceProxy['state']`
|
|
16970
|
-
* bag (typed against the codegen, so `s.battery` etc. autocomplete)
|
|
16971
|
-
* and returns one slice handle. Returns the slice value, undefined
|
|
16972
|
-
* while the binding resolves OR when the slice has no cached push yet.
|
|
16973
|
-
*
|
|
16974
|
-
* Idiomatic call-site:
|
|
16975
|
-
*
|
|
16976
|
-
* const battery = useDeviceState(trpc, deviceId, (s) => s.battery)
|
|
16977
|
-
* const zones = useDeviceState(trpc, deviceId, (s) => s.zones)
|
|
16978
|
-
*
|
|
16979
|
-
* vs. the manual pair:
|
|
16980
|
-
*
|
|
16981
|
-
* const dev = useDeviceProxy(trpc, deviceId)
|
|
16982
|
-
* const battery = useDeviceStateSlice(dev?.state.battery)
|
|
16983
|
-
* const zones = useDeviceStateSlice(dev?.state.zones)
|
|
16984
|
-
*
|
|
16985
|
-
* Use the manual pair when:
|
|
16986
|
-
* - you also need the proxy's cap method bag (`dev.zones?.addZone(...)`)
|
|
16987
|
-
* - you want to share the proxy across multiple slice reads with a
|
|
16988
|
-
* single binding fetch (the combinator calls useDeviceProxy
|
|
16989
|
-
* internally, which dedups bindings via React Query in practice
|
|
16990
|
-
* but spawns one extra subscription per call site)
|
|
16991
|
-
*
|
|
16992
|
-
* Use the combinator when reading just one slice — most common case.
|
|
16993
|
-
*/
|
|
16994
|
-
function useDeviceState(trpc, deviceId, select) {
|
|
16995
|
-
const dev = useDeviceProxy(trpc, deviceId);
|
|
16996
|
-
return useDeviceStateSlice(dev ? select(dev.state) : void 0);
|
|
16997
|
-
}
|
|
16998
|
-
//#endregion
|
|
16999
17059
|
//#region src/hooks/use-device-capability.ts
|
|
17000
17060
|
function useDeviceCapability(options) {
|
|
17001
17061
|
const dev = useDeviceProxy(options.trpc, options.deviceId);
|
|
@@ -17638,7 +17698,7 @@ function usePTZ(trpc, deviceId, hookOptions) {
|
|
|
17638
17698
|
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
17639
17699
|
const [presets, setPresets] = (0, react$1.useState)([]);
|
|
17640
17700
|
const [homePreset, setHomePresetState] = (0, react$1.useState)(null);
|
|
17641
|
-
const [
|
|
17701
|
+
const [homeReturn, setHomeReturnState] = (0, react$1.useState)(null);
|
|
17642
17702
|
const [options, setOptions] = (0, react$1.useState)(null);
|
|
17643
17703
|
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
17644
17704
|
const [error, setError] = (0, react$1.useState)(null);
|
|
@@ -17663,7 +17723,7 @@ function usePTZ(trpc, deviceId, hookOptions) {
|
|
|
17663
17723
|
const refreshHomeReturn = (0, react$1.useCallback)(async () => {
|
|
17664
17724
|
if (!enabled || !ptz) return;
|
|
17665
17725
|
try {
|
|
17666
|
-
setHomeReturnState(await ptz.
|
|
17726
|
+
setHomeReturnState(await ptz.getHomeReturn({}));
|
|
17667
17727
|
} catch (err) {
|
|
17668
17728
|
if (isAbsentProvider(err)) return;
|
|
17669
17729
|
setHomeReturnState(null);
|
|
@@ -17781,10 +17841,13 @@ function usePTZ(trpc, deviceId, hookOptions) {
|
|
|
17781
17841
|
wrap,
|
|
17782
17842
|
refreshHomePreset
|
|
17783
17843
|
]),
|
|
17784
|
-
|
|
17785
|
-
|
|
17844
|
+
homeReturn,
|
|
17845
|
+
setHomeReturn: (0, react$1.useCallback)(async (enabled, seconds) => {
|
|
17786
17846
|
if (!ptz) return;
|
|
17787
|
-
await wrap(() => ptz.
|
|
17847
|
+
await wrap(() => ptz.setHomeReturn({
|
|
17848
|
+
enabled,
|
|
17849
|
+
seconds
|
|
17850
|
+
}));
|
|
17788
17851
|
await refreshHomeReturn();
|
|
17789
17852
|
}, [
|
|
17790
17853
|
ptz,
|
|
@@ -17885,7 +17948,7 @@ function usePTZ(trpc, deviceId, hookOptions) {
|
|
|
17885
17948
|
*/
|
|
17886
17949
|
var SELECT_CLASS$2 = "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50";
|
|
17887
17950
|
function HomePresetPicker({ controls }) {
|
|
17888
|
-
const { homePreset, presets, setHomePreset, captureHomeHere,
|
|
17951
|
+
const { homePreset, presets, setHomePreset, captureHomeHere, homeReturn, setHomeReturn, busy } = controls;
|
|
17889
17952
|
const [saving, setSaving] = (0, react$1.useState)(false);
|
|
17890
17953
|
if (homePreset === null) return null;
|
|
17891
17954
|
const disabled = busy || saving;
|
|
@@ -17926,23 +17989,37 @@ function HomePresetPicker({ controls }) {
|
|
|
17926
17989
|
className: "self-start rounded-md border border-border px-2 py-1 text-[11px] text-foreground hover:bg-primary/10 disabled:opacity-50",
|
|
17927
17990
|
children: "Set home here"
|
|
17928
17991
|
}),
|
|
17929
|
-
|
|
17992
|
+
homeReturn !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17930
17993
|
className: "flex flex-col gap-1",
|
|
17931
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
17932
|
-
className: "
|
|
17933
|
-
children:
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17994
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17995
|
+
className: "flex items-center gap-2",
|
|
17996
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17997
|
+
type: "checkbox",
|
|
17998
|
+
checked: homeReturn.enabled,
|
|
17999
|
+
disabled,
|
|
18000
|
+
onChange: (e) => run(() => setHomeReturn(e.target.checked, homeReturn.seconds))
|
|
18001
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18002
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
18003
|
+
children: "Return home when idle"
|
|
18004
|
+
})]
|
|
18005
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18006
|
+
className: "flex flex-col gap-1",
|
|
18007
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18008
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
18009
|
+
children: "After (s)"
|
|
18010
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18011
|
+
type: "number",
|
|
18012
|
+
min: 0,
|
|
18013
|
+
max: 3600,
|
|
18014
|
+
defaultValue: homeReturn.seconds,
|
|
18015
|
+
disabled,
|
|
18016
|
+
onBlur: (e) => {
|
|
18017
|
+
const next = Number(e.target.value);
|
|
18018
|
+
if (!Number.isFinite(next) || Math.round(next) === homeReturn.seconds) return;
|
|
18019
|
+
run(() => setHomeReturn(homeReturn.enabled, Math.round(next)));
|
|
18020
|
+
},
|
|
18021
|
+
className: SELECT_CLASS$2
|
|
18022
|
+
})]
|
|
17946
18023
|
})]
|
|
17947
18024
|
}),
|
|
17948
18025
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -52588,7 +52665,7 @@ exports.usePtzCaptureHomeHere = usePtzCaptureHomeHere;
|
|
|
52588
52665
|
exports.usePtzContinuousMove = usePtzContinuousMove;
|
|
52589
52666
|
exports.usePtzDeletePreset = usePtzDeletePreset;
|
|
52590
52667
|
exports.usePtzGetHomePreset = usePtzGetHomePreset;
|
|
52591
|
-
exports.
|
|
52668
|
+
exports.usePtzGetHomeReturn = usePtzGetHomeReturn;
|
|
52592
52669
|
exports.usePtzGetOptions = usePtzGetOptions;
|
|
52593
52670
|
exports.usePtzGetPosition = usePtzGetPosition;
|
|
52594
52671
|
exports.usePtzGetPresets = usePtzGetPresets;
|
|
@@ -52599,7 +52676,7 @@ exports.usePtzMove = usePtzMove;
|
|
|
52599
52676
|
exports.usePtzSavePreset = usePtzSavePreset;
|
|
52600
52677
|
exports.usePtzSetAutofocus = usePtzSetAutofocus;
|
|
52601
52678
|
exports.usePtzSetHomePreset = usePtzSetHomePreset;
|
|
52602
|
-
exports.
|
|
52679
|
+
exports.usePtzSetHomeReturn = usePtzSetHomeReturn;
|
|
52603
52680
|
exports.usePtzStop = usePtzStop;
|
|
52604
52681
|
exports.useRebootReboot = useRebootReboot;
|
|
52605
52682
|
exports.useRecordedPlayback = useRecordedPlayback;
|