@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/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.getHomeReturnSeconds.useQuery`. */
6629
- var usePtzGetHomeReturnSeconds = trpc.ptz.getHomeReturnSeconds.useQuery;
6630
- /** Generated alias around `trpc.ptz.setHomeReturnSeconds.useMutation`. */
6631
- var usePtzSetHomeReturnSeconds = trpc.ptz.setHomeReturnSeconds.useMutation;
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
- /** Parse the `getGridForDevice` reply. See {@link GridLayoutReadOutcome}. */
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 (!isRecord(raw) || !("grid" in raw)) return "unreadable";
16452
- const grid = raw["grid"];
16453
- if (grid === null) return null;
16454
- if (!isRecord(grid)) return "unreadable";
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 outcome = parseGridView(await system.trpcClient.addons.custom.mutate({
16549
- addonId: ADDON_ID,
16550
- action: "getGridForDevice",
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
- }, [system, deviceId]);
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
- if (draft === null) return;
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 system.trpcClient.addons.custom.mutate({
16567
- addonId: ADDON_ID,
16568
- action: "saveGridLayout",
16569
- input: {
16570
- deviceId,
16571
- name: draft.name,
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 [homeReturnSeconds, setHomeReturnState] = (0, react$1.useState)(null);
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.getHomeReturnSeconds({}));
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
- homeReturnSeconds,
17785
- setHomeReturnSeconds: (0, react$1.useCallback)(async (seconds) => {
17844
+ homeReturn,
17845
+ setHomeReturn: (0, react$1.useCallback)(async (enabled, seconds) => {
17786
17846
  if (!ptz) return;
17787
- await wrap(() => ptz.setHomeReturnSeconds({ seconds }));
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, homeReturnSeconds, setHomeReturnSeconds, busy } = controls;
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
- homeReturnSeconds !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
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.jsx)("span", {
17932
- className: "text-[10px] font-medium text-foreground-subtle",
17933
- children: "Return home after (s)"
17934
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
17935
- type: "number",
17936
- min: 0,
17937
- max: 3600,
17938
- defaultValue: homeReturnSeconds,
17939
- disabled,
17940
- onBlur: (e) => {
17941
- const next = Number(e.target.value);
17942
- if (!Number.isFinite(next) || next === homeReturnSeconds) return;
17943
- run(() => setHomeReturnSeconds(Math.round(next)));
17944
- },
17945
- className: SELECT_CLASS$2
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.usePtzGetHomeReturnSeconds = usePtzGetHomeReturnSeconds;
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.usePtzSetHomeReturnSeconds = usePtzSetHomeReturnSeconds;
52679
+ exports.usePtzSetHomeReturn = usePtzSetHomeReturn;
52603
52680
  exports.usePtzStop = usePtzStop;
52604
52681
  exports.useRebootReboot = useRebootReboot;
52605
52682
  exports.useRecordedPlayback = useRecordedPlayback;