@camstack/ui-library 0.1.52 → 0.1.53
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/AutotrackSection.d.ts +5 -0
- package/dist/composites/cap-settings/MotionGridCanvas.d.ts +9 -0
- package/dist/composites/cap-settings/MotionZonesSettings.d.ts +2 -0
- package/dist/composites/cap-settings/PtzPanel.d.ts +2 -0
- package/dist/composites/cap-settings/index.d.ts +14 -0
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/state-values-stream.d.ts +14 -18
- package/dist/contexts/widget-registry.d.ts +67 -6
- package/dist/generated/system-hooks.d.ts +54 -2
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/use-device-autotrack.d.ts +10 -0
- package/dist/hooks/use-ptz.d.ts +29 -1
- package/dist/index.cjs +2103 -1111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2068 -1111
- package/dist/index.js.map +1 -1
- package/dist/widgets/host-widgets.d.ts +9 -0
- package/package.json +9 -1
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@ import * as __mfHostTrpcClient from "@trpc/client";
|
|
|
13
13
|
import { createTRPCClient, createWSClient, httpLink, splitLink, wsLink } from "@trpc/client";
|
|
14
14
|
import * as __mfHostTrpcReactQuery from "@trpc/react-query";
|
|
15
15
|
import { createTRPCReact } from "@trpc/react-query";
|
|
16
|
+
import * as __mfHostReactKonva from "react-konva";
|
|
17
|
+
import * as __mfHostKonva from "konva";
|
|
16
18
|
import { ALL_CAPABILITY_DEFINITIONS, DeviceExportStatusSchema, EventCategory, ExposedDeviceSchema, KNOWN_CAP_NAMES, createDeviceProxy } from "@camstack/types";
|
|
17
19
|
import { createSystem } from "@camstack/sdk";
|
|
18
20
|
import { z } from "zod";
|
|
@@ -6851,6 +6853,8 @@ function populateShareCache() {
|
|
|
6851
6853
|
cache.share["@tanstack/react-query"] ??= __mfHostTanstackQuery;
|
|
6852
6854
|
cache.share["@trpc/client"] ??= __mfHostTrpcClient;
|
|
6853
6855
|
cache.share["@trpc/react-query"] ??= __mfHostTrpcReactQuery;
|
|
6856
|
+
cache.share["react-konva"] ??= __mfHostReactKonva;
|
|
6857
|
+
cache.share["konva"] ??= __mfHostKonva;
|
|
6854
6858
|
cache.share["@camstack/ui-library"] ??= readHostGlobal("__camstackUiLibrary");
|
|
6855
6859
|
cache.share["@camstack/sdk"] ??= readHostGlobal("__camstackSdk");
|
|
6856
6860
|
cache.share["@camstack/types"] ??= readHostGlobal("__camstackTypes");
|
|
@@ -6869,6 +6873,8 @@ function ensureMfHostInit() {
|
|
|
6869
6873
|
"@tanstack/react-query": npmShared(() => __mfHostTanstackQuery),
|
|
6870
6874
|
"@trpc/client": npmShared(() => __mfHostTrpcClient),
|
|
6871
6875
|
"@trpc/react-query": npmShared(() => __mfHostTrpcReactQuery),
|
|
6876
|
+
"react-konva": npmShared(() => __mfHostReactKonva),
|
|
6877
|
+
"konva": npmShared(() => __mfHostKonva),
|
|
6872
6878
|
"@camstack/ui-library": npmShared(() => readHostGlobal("__camstackUiLibrary")),
|
|
6873
6879
|
"@camstack/sdk": npmShared(() => readHostGlobal("__camstackSdk")),
|
|
6874
6880
|
"@camstack/types": npmShared(() => readHostGlobal("__camstackTypes"))
|
|
@@ -15924,6 +15930,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
|
|
|
15924
15930
|
var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
|
|
15925
15931
|
/** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
|
|
15926
15932
|
var useDecoderPullFrames = trpc.decoder.pullFrames.useQuery;
|
|
15933
|
+
/** Generated alias around `trpc.decoder.pullHandles.useQuery`. */
|
|
15934
|
+
var useDecoderPullHandles = trpc.decoder.pullHandles.useQuery;
|
|
15935
|
+
/** Generated alias around `trpc.decoder.getFrame.useQuery`. */
|
|
15936
|
+
var useDecoderGetFrame = trpc.decoder.getFrame.useQuery;
|
|
15937
|
+
/** Generated alias around `trpc.decoder.getShmStats.useQuery`. */
|
|
15938
|
+
var useDecoderGetShmStats = trpc.decoder.getShmStats.useQuery;
|
|
15927
15939
|
/** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
|
|
15928
15940
|
var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
|
|
15929
15941
|
/** Generated alias around `trpc.decoder.getStats.useQuery`. */
|
|
@@ -16208,6 +16220,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
|
|
|
16208
16220
|
var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
|
|
16209
16221
|
/** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
|
|
16210
16222
|
var useMotionTriggerGetStatus = trpc.motionTrigger.getStatus.useQuery;
|
|
16223
|
+
/** Generated alias around `trpc.motionZones.getOptions.useQuery`. */
|
|
16224
|
+
var useMotionZonesGetOptions = trpc.motionZones.getOptions.useQuery;
|
|
16225
|
+
/** Generated alias around `trpc.motionZones.setZone.useMutation`. */
|
|
16226
|
+
var useMotionZonesSetZone = trpc.motionZones.setZone.useMutation;
|
|
16227
|
+
/** Generated alias around `trpc.motionZones.getStatus.useQuery`. */
|
|
16228
|
+
var useMotionZonesGetStatus = trpc.motionZones.getStatus.useQuery;
|
|
16211
16229
|
/** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
|
|
16212
16230
|
var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
|
|
16213
16231
|
/** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
|
|
@@ -16260,6 +16278,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
|
|
|
16260
16278
|
var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
|
|
16261
16279
|
/** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
|
|
16262
16280
|
var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
|
|
16281
|
+
/** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
|
|
16282
|
+
var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
|
|
16263
16283
|
/** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
|
|
16264
16284
|
var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
|
|
16265
16285
|
/** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
|
|
@@ -16478,10 +16498,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
|
|
|
16478
16498
|
var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
|
|
16479
16499
|
/** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
|
|
16480
16500
|
var usePtzGoToPreset = trpc.ptz.goToPreset.useMutation;
|
|
16501
|
+
/** Generated alias around `trpc.ptz.savePreset.useMutation`. */
|
|
16502
|
+
var usePtzSavePreset = trpc.ptz.savePreset.useMutation;
|
|
16503
|
+
/** Generated alias around `trpc.ptz.deletePreset.useMutation`. */
|
|
16504
|
+
var usePtzDeletePreset = trpc.ptz.deletePreset.useMutation;
|
|
16505
|
+
/** Generated alias around `trpc.ptz.getOptions.useQuery`. */
|
|
16506
|
+
var usePtzGetOptions = trpc.ptz.getOptions.useQuery;
|
|
16481
16507
|
/** Generated alias around `trpc.ptz.goHome.useMutation`. */
|
|
16482
16508
|
var usePtzGoHome = trpc.ptz.goHome.useMutation;
|
|
16483
16509
|
/** Generated alias around `trpc.ptz.getPosition.useQuery`. */
|
|
16484
16510
|
var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
|
|
16511
|
+
/** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
|
|
16512
|
+
var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
|
|
16485
16513
|
/** Generated alias around `trpc.ptz.getStatus.useQuery`. */
|
|
16486
16514
|
var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
|
|
16487
16515
|
/** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
|
|
@@ -16638,8 +16666,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
|
|
|
16638
16666
|
var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
|
|
16639
16667
|
/** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
|
|
16640
16668
|
var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
|
|
16641
|
-
/** Generated alias around `trpc.streamBroker.
|
|
16642
|
-
var
|
|
16669
|
+
/** Generated alias around `trpc.streamBroker.subscribeAudioChunks.useMutation`. */
|
|
16670
|
+
var useStreamBrokerSubscribeAudioChunks = trpc.streamBroker.subscribeAudioChunks.useMutation;
|
|
16671
|
+
/** Generated alias around `trpc.streamBroker.pullAudioChunks.useQuery`. */
|
|
16672
|
+
var useStreamBrokerPullAudioChunks = trpc.streamBroker.pullAudioChunks.useQuery;
|
|
16673
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeAudioChunks.useMutation`. */
|
|
16674
|
+
var useStreamBrokerUnsubscribeAudioChunks = trpc.streamBroker.unsubscribeAudioChunks.useMutation;
|
|
16675
|
+
/** Generated alias around `trpc.streamBroker.subscribeFrames.useMutation`. */
|
|
16676
|
+
var useStreamBrokerSubscribeFrames = trpc.streamBroker.subscribeFrames.useMutation;
|
|
16677
|
+
/** Generated alias around `trpc.streamBroker.pullFrameHandles.useQuery`. */
|
|
16678
|
+
var useStreamBrokerPullFrameHandles = trpc.streamBroker.pullFrameHandles.useQuery;
|
|
16679
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeFrames.useMutation`. */
|
|
16680
|
+
var useStreamBrokerUnsubscribeFrames = trpc.streamBroker.unsubscribeFrames.useMutation;
|
|
16643
16681
|
/** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
|
|
16644
16682
|
var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
|
|
16645
16683
|
/** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
|
|
@@ -16662,6 +16700,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
|
|
|
16662
16700
|
var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
|
|
16663
16701
|
/** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
|
|
16664
16702
|
var useStreamBrokerApplyDeviceSettingsPatch = trpc.streamBroker.applyDeviceSettingsPatch.useMutation;
|
|
16703
|
+
/** Generated alias around `trpc.streamParams.getOptions.useQuery`. */
|
|
16704
|
+
var useStreamParamsGetOptions = trpc.streamParams.getOptions.useQuery;
|
|
16705
|
+
/** Generated alias around `trpc.streamParams.setProfile.useMutation`. */
|
|
16706
|
+
var useStreamParamsSetProfile = trpc.streamParams.setProfile.useMutation;
|
|
16707
|
+
/** Generated alias around `trpc.streamParams.getConfigSchema.useQuery`. */
|
|
16708
|
+
var useStreamParamsGetConfigSchema = trpc.streamParams.getConfigSchema.useQuery;
|
|
16709
|
+
/** Generated alias around `trpc.streamParams.getStatus.useQuery`. */
|
|
16710
|
+
var useStreamParamsGetStatus = trpc.streamParams.getStatus.useQuery;
|
|
16665
16711
|
/** Generated alias around `trpc.switch.setState.useMutation`. */
|
|
16666
16712
|
var useSwitchSetState = trpc.switch.setState.useMutation;
|
|
16667
16713
|
/** Generated alias around `trpc.switch.getStatus.useQuery`. */
|
|
@@ -16724,6 +16770,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
|
|
|
16724
16770
|
var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
|
|
16725
16771
|
/** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
|
|
16726
16772
|
var useUserManagementVerifyTotp = trpc.userManagement.verifyTotp.useMutation;
|
|
16773
|
+
/** Generated alias around `trpc.userManagement.oauthIssueCode.useMutation`. */
|
|
16774
|
+
var useUserManagementOauthIssueCode = trpc.userManagement.oauthIssueCode.useMutation;
|
|
16775
|
+
/** Generated alias around `trpc.userManagement.oauthExchangeCode.useMutation`. */
|
|
16776
|
+
var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.useMutation;
|
|
16777
|
+
/** Generated alias around `trpc.userManagement.oauthRefresh.useMutation`. */
|
|
16778
|
+
var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
|
|
16779
|
+
/** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
|
|
16780
|
+
var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
|
|
16781
|
+
/** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
|
|
16782
|
+
var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
|
|
16783
|
+
/** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
|
|
16784
|
+
var useUserManagementRevokeOauthSession = trpc.userManagement.revokeOauthSession.useMutation;
|
|
16727
16785
|
/** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
|
|
16728
16786
|
var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
|
|
16729
16787
|
/** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
|
|
@@ -16767,8 +16825,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16767
16825
|
* `vite.widgets.config.ts`). At runtime we fetch the public list of
|
|
16768
16826
|
* addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
|
|
16769
16827
|
* each remote (`registerRemotes`) once, then resolve a widget by
|
|
16770
|
-
* loading the exposed
|
|
16771
|
-
*
|
|
16828
|
+
* loading the exposed module (`loadRemote`) whose default export is a
|
|
16829
|
+
* `Record<componentKey, ComponentType>` map.
|
|
16830
|
+
*
|
|
16831
|
+
* Unified UI-contribution model (Task 10) — a widget declaration IS a
|
|
16832
|
+
* `UiContribution` with `kind:'remote'`. Both the device-detail path
|
|
16833
|
+
* (`ContributionRenderer`) and the dashboard render widgets through the
|
|
16834
|
+
* same `loadRemoteBundle` MF path. `loadRemoteBundle` is exported and
|
|
16835
|
+
* generic over `(remoteName, exposedModule, entryUrl)` so any
|
|
16836
|
+
* `UiContributionRemote` descriptor resolves without an aggregator-keyed
|
|
16837
|
+
* lookup.
|
|
16772
16838
|
*
|
|
16773
16839
|
* Why MF instead of raw ESM:
|
|
16774
16840
|
* - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
|
|
@@ -16778,9 +16844,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16778
16844
|
* wins by default).
|
|
16779
16845
|
* - cross-bundle context invariant preserved automatically: every
|
|
16780
16846
|
* remote's `@camstack/ui-library` import resolves to the host's
|
|
16781
|
-
* instance, so `createContext()` references match across bundles
|
|
16782
|
-
* (the duplicate-Context bug that motivated `createSharedContext`
|
|
16783
|
-
* falls out for free).
|
|
16847
|
+
* instance, so `createContext()` references match across bundles.
|
|
16784
16848
|
*
|
|
16785
16849
|
* Live-update — listens to `addon.widget-ready` so newly-loaded addons
|
|
16786
16850
|
* surface their widgets without a manual page reload (the aggregator
|
|
@@ -16791,11 +16855,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16791
16855
|
*/
|
|
16792
16856
|
var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
|
|
16793
16857
|
/**
|
|
16794
|
-
* Process-global cache for resolved
|
|
16795
|
-
*
|
|
16796
|
-
* default
|
|
16797
|
-
* provider remount — avoids a full re-fetch + re-init of the
|
|
16798
|
-
* when admin-ui's tree cycles.
|
|
16858
|
+
* Process-global cache for resolved remote bundles, keyed by
|
|
16859
|
+
* `<remoteName>::<exposedModule>`. Each cached value is the exposed
|
|
16860
|
+
* module's default-record map (`Record<componentKey, Component>`).
|
|
16861
|
+
* Survives provider remount — avoids a full re-fetch + re-init of the
|
|
16862
|
+
* MF remote when admin-ui's tree cycles.
|
|
16799
16863
|
*/
|
|
16800
16864
|
var bundleModuleCache = /* @__PURE__ */ new Map();
|
|
16801
16865
|
var bundleInflight = /* @__PURE__ */ new Map();
|
|
@@ -16807,6 +16871,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
|
|
|
16807
16871
|
* update.
|
|
16808
16872
|
*/
|
|
16809
16873
|
var registeredRemotes = /* @__PURE__ */ new Set();
|
|
16874
|
+
/** Cache key for `bundleModuleCache` — one entry per `(remote, module)`. */
|
|
16875
|
+
function bundleCacheKey(remoteName, exposedModule) {
|
|
16876
|
+
return `${remoteName}::${exposedModule}`;
|
|
16877
|
+
}
|
|
16810
16878
|
function isRemoteWidgetsModule(value) {
|
|
16811
16879
|
if (!value || typeof value !== "object") return false;
|
|
16812
16880
|
const def = value.default;
|
|
@@ -16815,8 +16883,7 @@ function isRemoteWidgetsModule(value) {
|
|
|
16815
16883
|
/**
|
|
16816
16884
|
* Diagnostic-only string for the "Got: …" tail in the not-a-record
|
|
16817
16885
|
* error. Returns a JSON-friendly snapshot of the module's keys + proto
|
|
16818
|
-
* name without exposing the value itself
|
|
16819
|
-
* thing the module factory produced, may be huge or contain PII).
|
|
16886
|
+
* name without exposing the value itself.
|
|
16820
16887
|
*/
|
|
16821
16888
|
function describeRemoteShape(mod) {
|
|
16822
16889
|
if (!mod || typeof mod !== "object") return typeof mod;
|
|
@@ -16830,13 +16897,19 @@ function describeRemoteShape(mod) {
|
|
|
16830
16897
|
};
|
|
16831
16898
|
}
|
|
16832
16899
|
/**
|
|
16833
|
-
* Register the MF remote (idempotent) and load its
|
|
16834
|
-
* Returns the resolved `Record<
|
|
16900
|
+
* Register the MF remote (idempotent) and load one of its exposed
|
|
16901
|
+
* modules. Returns the resolved `Record<componentKey, Component>` map
|
|
16902
|
+
* (the exposed module's `default` export).
|
|
16903
|
+
*
|
|
16904
|
+
* Exported so the unified `ContributionRenderer` `kind:'remote'` path
|
|
16905
|
+
* can load an arbitrary `{ remoteName, exposedModule }` descriptor —
|
|
16906
|
+
* the same path the `WidgetRegistry` uses internally for widgets.
|
|
16835
16907
|
*/
|
|
16836
|
-
async function loadRemoteBundle(remoteName, entryUrl) {
|
|
16837
|
-
const
|
|
16908
|
+
async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
|
|
16909
|
+
const cacheKey = bundleCacheKey(remoteName, exposedModule);
|
|
16910
|
+
const cached = bundleModuleCache.get(cacheKey);
|
|
16838
16911
|
if (cached) return cached;
|
|
16839
|
-
const inflight = bundleInflight.get(
|
|
16912
|
+
const inflight = bundleInflight.get(cacheKey);
|
|
16840
16913
|
if (inflight) return inflight;
|
|
16841
16914
|
if (!registeredRemotes.has(remoteName)) {
|
|
16842
16915
|
ensureMfHostInit();
|
|
@@ -16847,22 +16920,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
|
|
|
16847
16920
|
}], { force: false });
|
|
16848
16921
|
registeredRemotes.add(remoteName);
|
|
16849
16922
|
}
|
|
16850
|
-
const promise = loadRemote(`${remoteName}
|
|
16923
|
+
const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
|
|
16851
16924
|
if (!isRemoteWidgetsModule(mod)) {
|
|
16852
16925
|
const shape = describeRemoteShape(mod);
|
|
16853
|
-
throw new Error(`
|
|
16926
|
+
throw new Error(`Remote ${remoteName} (${entryUrl}) does not expose a default record on '${exposedModule}'. Got: ${JSON.stringify(shape)}`);
|
|
16854
16927
|
}
|
|
16855
16928
|
const map = mod.default;
|
|
16856
|
-
bundleModuleCache.set(
|
|
16857
|
-
bundleInflight.delete(
|
|
16929
|
+
bundleModuleCache.set(cacheKey, map);
|
|
16930
|
+
bundleInflight.delete(cacheKey);
|
|
16858
16931
|
return map;
|
|
16859
16932
|
}).catch((err) => {
|
|
16860
|
-
bundleInflight.delete(
|
|
16933
|
+
bundleInflight.delete(cacheKey);
|
|
16861
16934
|
throw err;
|
|
16862
16935
|
});
|
|
16863
|
-
bundleInflight.set(
|
|
16936
|
+
bundleInflight.set(cacheKey, promise);
|
|
16864
16937
|
return promise;
|
|
16865
16938
|
}
|
|
16939
|
+
/** Synchronous peek at the resolved-bundle cache — `undefined` if not yet loaded. */
|
|
16940
|
+
function peekBundle(remoteName, exposedModule) {
|
|
16941
|
+
return bundleModuleCache.get(bundleCacheKey(remoteName, exposedModule));
|
|
16942
|
+
}
|
|
16866
16943
|
var BOOT_WINDOW_MS = 3e4;
|
|
16867
16944
|
var POLL_INTERVAL_MS = 2e3;
|
|
16868
16945
|
function WidgetRegistryProvider({ children }) {
|
|
@@ -16882,21 +16959,24 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16882
16959
|
queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
|
|
16883
16960
|
});
|
|
16884
16961
|
const [resolvedTick, setResolvedTick] = useState(0);
|
|
16962
|
+
const widgets = useMemo(() => rawWidgets ?? [], [rawWidgets]);
|
|
16885
16963
|
useEffect(() => {
|
|
16886
|
-
if (
|
|
16964
|
+
if (widgets.length === 0) return;
|
|
16887
16965
|
let cancelled = false;
|
|
16888
|
-
const
|
|
16889
|
-
for (const w of
|
|
16890
|
-
|
|
16891
|
-
|
|
16892
|
-
|
|
16966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16967
|
+
for (const w of widgets) {
|
|
16968
|
+
const key = bundleCacheKey(w.remote.remoteName, w.remote.exposedModule);
|
|
16969
|
+
if (seen.has(key)) continue;
|
|
16970
|
+
seen.add(key);
|
|
16971
|
+
if (bundleModuleCache.has(key)) continue;
|
|
16893
16972
|
const entryUrl = w.bundleUrl;
|
|
16894
|
-
loadRemoteBundle(w.remoteName, entryUrl).then(() => {
|
|
16973
|
+
loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
|
|
16895
16974
|
if (!cancelled) setResolvedTick((t) => t + 1);
|
|
16896
16975
|
}).catch((err) => {
|
|
16897
16976
|
const reason = err instanceof Error ? err.message : String(err);
|
|
16898
16977
|
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
|
|
16899
|
-
remoteName: w.remoteName,
|
|
16978
|
+
remoteName: w.remote.remoteName,
|
|
16979
|
+
exposedModule: w.remote.exposedModule,
|
|
16900
16980
|
entryUrl,
|
|
16901
16981
|
reason
|
|
16902
16982
|
});
|
|
@@ -16905,19 +16985,26 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16905
16985
|
return () => {
|
|
16906
16986
|
cancelled = true;
|
|
16907
16987
|
};
|
|
16908
|
-
}, [
|
|
16988
|
+
}, [widgets]);
|
|
16909
16989
|
const registry = useMemo(() => {
|
|
16910
|
-
const widgets = rawWidgets ?? [];
|
|
16911
16990
|
const byId = /* @__PURE__ */ new Map();
|
|
16912
|
-
|
|
16991
|
+
const entryUrlByRemote = /* @__PURE__ */ new Map();
|
|
16992
|
+
for (const w of widgets) {
|
|
16993
|
+
byId.set(`${w.addonId}/${w.stableId}`, w);
|
|
16994
|
+
entryUrlByRemote.set(w.remote.remoteName, w.bundleUrl);
|
|
16995
|
+
}
|
|
16913
16996
|
const toMetadata = (widgetId, entry) => ({
|
|
16914
16997
|
widgetId,
|
|
16915
16998
|
addonId: entry.addonId,
|
|
16916
16999
|
stableId: entry.stableId,
|
|
17000
|
+
tab: entry.tab,
|
|
17001
|
+
subTab: entry.subTab,
|
|
16917
17002
|
label: entry.label,
|
|
17003
|
+
order: entry.order,
|
|
17004
|
+
kind: entry.kind,
|
|
17005
|
+
remote: entry.remote,
|
|
16918
17006
|
description: entry.description,
|
|
16919
17007
|
icon: entry.icon,
|
|
16920
|
-
remoteName: entry.remoteName,
|
|
16921
17008
|
bundleUrl: entry.bundleUrl,
|
|
16922
17009
|
hosts: entry.hosts,
|
|
16923
17010
|
requires: entry.requires,
|
|
@@ -16930,9 +17017,9 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16930
17017
|
resolve: (widgetId) => {
|
|
16931
17018
|
const entry = byId.get(widgetId);
|
|
16932
17019
|
if (!entry) return void 0;
|
|
16933
|
-
const bundle =
|
|
17020
|
+
const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
|
|
16934
17021
|
if (!bundle) return null;
|
|
16935
|
-
const Component = bundle[entry.stableId];
|
|
17022
|
+
const Component = bundle[entry.remote.componentKey ?? entry.stableId];
|
|
16936
17023
|
if (!Component) return void 0;
|
|
16937
17024
|
return Component;
|
|
16938
17025
|
},
|
|
@@ -16945,15 +17032,23 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16945
17032
|
const out = [];
|
|
16946
17033
|
for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
|
|
16947
17034
|
return out;
|
|
16948
|
-
}
|
|
17035
|
+
},
|
|
17036
|
+
entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
|
|
16949
17037
|
};
|
|
16950
|
-
}, [
|
|
17038
|
+
}, [widgets, resolvedTick]);
|
|
16951
17039
|
return /* @__PURE__ */ jsx(WidgetRegistryContext.Provider, {
|
|
16952
17040
|
value: registry,
|
|
16953
17041
|
children
|
|
16954
17042
|
});
|
|
16955
17043
|
}
|
|
16956
|
-
/**
|
|
17044
|
+
/**
|
|
17045
|
+
* Returns the registered widget component, `null` while loading, or
|
|
17046
|
+
* `undefined` if unknown.
|
|
17047
|
+
*
|
|
17048
|
+
* @deprecated Unused by all render paths since the unified
|
|
17049
|
+
* UI-contribution model (Task 10). Use `useRemoteComponent` /
|
|
17050
|
+
* `<WidgetSlot>` instead. Kept for external addons that may import it.
|
|
17051
|
+
*/
|
|
16957
17052
|
function useWidget(widgetId) {
|
|
16958
17053
|
return useWidgetRegistry().resolve(widgetId);
|
|
16959
17054
|
}
|
|
@@ -16967,6 +17062,68 @@ function useWidgetMetadata(widgetId) {
|
|
|
16967
17062
|
function useAllWidgets() {
|
|
16968
17063
|
return useWidgetRegistry().listAll();
|
|
16969
17064
|
}
|
|
17065
|
+
/**
|
|
17066
|
+
* Resolve a `kind:'remote'` `UiContributionRemote` descriptor to a
|
|
17067
|
+
* React component. Drives the unified `ContributionRenderer`
|
|
17068
|
+
* `kind:'remote'` path — both device-detail contributions and dashboard
|
|
17069
|
+
* widgets resolve through the same MF `loadRemoteBundle` loader.
|
|
17070
|
+
*
|
|
17071
|
+
* Returns:
|
|
17072
|
+
* - `undefined` — the remote couldn't be resolved (no entry URL known,
|
|
17073
|
+
* or the remote exposed no component for `componentKey`),
|
|
17074
|
+
* - `null` — the bundle is still loading,
|
|
17075
|
+
* - the component otherwise.
|
|
17076
|
+
*
|
|
17077
|
+
* `entryUrl` may be passed explicitly; when omitted it's resolved from
|
|
17078
|
+
* the registry's aggregator-stamped `bundleUrl` for `remote.remoteName`.
|
|
17079
|
+
*/
|
|
17080
|
+
function useRemoteComponent(remote, entryUrl) {
|
|
17081
|
+
const reg = useOptionalWidgetRegistry();
|
|
17082
|
+
const resolvedEntryUrl = entryUrl ?? reg?.entryUrlFor(remote.remoteName);
|
|
17083
|
+
const [tick, setTick] = useState(0);
|
|
17084
|
+
const [loadFailed, setLoadFailed] = useState(false);
|
|
17085
|
+
useEffect(() => {
|
|
17086
|
+
if (!resolvedEntryUrl) return;
|
|
17087
|
+
setLoadFailed(false);
|
|
17088
|
+
if (peekBundle(remote.remoteName, remote.exposedModule)) return;
|
|
17089
|
+
let cancelled = false;
|
|
17090
|
+
loadRemoteBundle(remote.remoteName, remote.exposedModule, resolvedEntryUrl).then(() => {
|
|
17091
|
+
if (!cancelled) setTick((t) => t + 1);
|
|
17092
|
+
}).catch((err) => {
|
|
17093
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
17094
|
+
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load remote component", {
|
|
17095
|
+
remoteName: remote.remoteName,
|
|
17096
|
+
exposedModule: remote.exposedModule,
|
|
17097
|
+
entryUrl: resolvedEntryUrl,
|
|
17098
|
+
reason
|
|
17099
|
+
});
|
|
17100
|
+
if (!cancelled) setLoadFailed(true);
|
|
17101
|
+
});
|
|
17102
|
+
return () => {
|
|
17103
|
+
cancelled = true;
|
|
17104
|
+
};
|
|
17105
|
+
}, [
|
|
17106
|
+
remote.remoteName,
|
|
17107
|
+
remote.exposedModule,
|
|
17108
|
+
resolvedEntryUrl
|
|
17109
|
+
]);
|
|
17110
|
+
return useMemo(() => {
|
|
17111
|
+
if (!resolvedEntryUrl) return void 0;
|
|
17112
|
+
if (loadFailed) return void 0;
|
|
17113
|
+
const bundle = peekBundle(remote.remoteName, remote.exposedModule);
|
|
17114
|
+
if (!bundle) return null;
|
|
17115
|
+
const componentKey = remote.componentKey;
|
|
17116
|
+
if (componentKey === void 0) return Object.values(bundle)[0] ?? void 0;
|
|
17117
|
+
return bundle[componentKey] ?? void 0;
|
|
17118
|
+
}, [
|
|
17119
|
+
remote.remoteName,
|
|
17120
|
+
remote.exposedModule,
|
|
17121
|
+
remote.componentKey,
|
|
17122
|
+
resolvedEntryUrl,
|
|
17123
|
+
tick,
|
|
17124
|
+
loadFailed
|
|
17125
|
+
]);
|
|
17126
|
+
}
|
|
16970
17127
|
/** Read the registry instance — throws when no provider is mounted. */
|
|
16971
17128
|
function useWidgetRegistry() {
|
|
16972
17129
|
const ctx = useOptionalWidgetRegistry();
|
|
@@ -16981,163 +17138,1429 @@ function useContextSafe(ctx) {
|
|
|
16981
17138
|
return useContext(ctx);
|
|
16982
17139
|
}
|
|
16983
17140
|
//#endregion
|
|
16984
|
-
//#region src/
|
|
17141
|
+
//#region src/hooks/use-ptz.ts
|
|
16985
17142
|
/**
|
|
16986
|
-
*
|
|
16987
|
-
* widget. Consumers reference a widget by its public id
|
|
16988
|
-
* (`<addonId>/<stableId>`), the slot looks the component up in the
|
|
16989
|
-
* shared `WidgetRegistry`, validates host context against the widget's
|
|
16990
|
-
* `requires` metadata, and renders one of:
|
|
16991
|
-
* - skeleton placeholder while the bundle is still loading,
|
|
16992
|
-
* - inline error fallback when the widget id is unknown OR the host
|
|
16993
|
-
* didn't supply a required context (`deviceContext` /
|
|
16994
|
-
* `integrationContext`),
|
|
16995
|
-
* - the resolved component otherwise.
|
|
17143
|
+
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
16996
17144
|
*
|
|
16997
|
-
*
|
|
16998
|
-
*
|
|
16999
|
-
*
|
|
17145
|
+
* Wraps the `ptz` capability methods through the canonical
|
|
17146
|
+
* `useDeviceProxy` surface — every call goes through
|
|
17147
|
+
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
17148
|
+
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
17149
|
+
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
17150
|
+
*
|
|
17151
|
+
* Returns:
|
|
17152
|
+
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
17153
|
+
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
17154
|
+
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
17155
|
+
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
17156
|
+
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
17157
|
+
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
17158
|
+
* - `goHome()`: jump to preset 0.
|
|
17159
|
+
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
17160
|
+
*
|
|
17161
|
+
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
17162
|
+
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
17163
|
+
* addon pages (AddonPageProps.trpc) alike.
|
|
17000
17164
|
*/
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
}
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
}
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17165
|
+
var DIRECTION_VECTORS = {
|
|
17166
|
+
"up": {
|
|
17167
|
+
pan: 0,
|
|
17168
|
+
tilt: 1
|
|
17169
|
+
},
|
|
17170
|
+
"down": {
|
|
17171
|
+
pan: 0,
|
|
17172
|
+
tilt: -1
|
|
17173
|
+
},
|
|
17174
|
+
"left": {
|
|
17175
|
+
pan: -1,
|
|
17176
|
+
tilt: 0
|
|
17177
|
+
},
|
|
17178
|
+
"right": {
|
|
17179
|
+
pan: 1,
|
|
17180
|
+
tilt: 0
|
|
17181
|
+
},
|
|
17182
|
+
"up-left": {
|
|
17183
|
+
pan: -1,
|
|
17184
|
+
tilt: 1
|
|
17185
|
+
},
|
|
17186
|
+
"up-right": {
|
|
17187
|
+
pan: 1,
|
|
17188
|
+
tilt: 1
|
|
17189
|
+
},
|
|
17190
|
+
"down-left": {
|
|
17191
|
+
pan: -1,
|
|
17192
|
+
tilt: -1
|
|
17193
|
+
},
|
|
17194
|
+
"down-right": {
|
|
17195
|
+
pan: 1,
|
|
17196
|
+
tilt: -1
|
|
17024
17197
|
}
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
}
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
17058
|
-
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
17059
|
-
const resolvedLabel = resolveLabel(label, translationFn);
|
|
17060
|
-
const resolvedDescription = resolveLabel(description, translationFn);
|
|
17061
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
17062
|
-
className: colSpanClass,
|
|
17063
|
-
children: [
|
|
17064
|
-
resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ jsxs("label", {
|
|
17065
|
-
className: LABEL_CLASS,
|
|
17066
|
-
children: [resolvedLabel, required && /* @__PURE__ */ jsx("span", {
|
|
17067
|
-
className: "text-danger ml-0.5",
|
|
17068
|
-
children: "*"
|
|
17069
|
-
})]
|
|
17070
|
-
}),
|
|
17071
|
-
children,
|
|
17072
|
-
resolvedDescription && /* @__PURE__ */ jsx("p", {
|
|
17073
|
-
className: DESC_CLASS,
|
|
17074
|
-
children: resolvedDescription
|
|
17075
|
-
})
|
|
17076
|
-
]
|
|
17077
|
-
});
|
|
17078
|
-
}
|
|
17079
|
-
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
17080
|
-
return /* @__PURE__ */ jsx(FieldWrapper, {
|
|
17081
|
-
label: field.label,
|
|
17082
|
-
description: field.description,
|
|
17083
|
-
required: field.required,
|
|
17084
|
-
span: field.span,
|
|
17085
|
-
translationFn,
|
|
17086
|
-
children: /* @__PURE__ */ jsx("input", {
|
|
17087
|
-
type: field.inputType ?? "text",
|
|
17088
|
-
className: INPUT_CLASS,
|
|
17089
|
-
value: value === void 0 || value === null ? "" : String(value),
|
|
17090
|
-
placeholder: field.placeholder,
|
|
17091
|
-
maxLength: field.maxLength,
|
|
17092
|
-
pattern: field.pattern,
|
|
17093
|
-
disabled: disabled || field.disabled,
|
|
17094
|
-
onChange: (e) => onChange(e.target.value)
|
|
17095
|
-
})
|
|
17096
|
-
});
|
|
17097
|
-
}
|
|
17098
|
-
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
17099
|
-
const [local, setLocal] = useState(value === void 0 || value === null ? "" : String(value));
|
|
17100
|
-
const focusedRef = useRef(false);
|
|
17198
|
+
};
|
|
17199
|
+
function usePTZ(trpc, deviceId, hookOptions) {
|
|
17200
|
+
const defaultSpeed = hookOptions?.defaultSpeed ?? .5;
|
|
17201
|
+
const pulseMs = hookOptions?.pulseMs ?? 250;
|
|
17202
|
+
const enabled = hookOptions?.enabled ?? true;
|
|
17203
|
+
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
17204
|
+
const [presets, setPresets] = useState([]);
|
|
17205
|
+
const [options, setOptions] = useState(null);
|
|
17206
|
+
const [busy, setBusy] = useState(false);
|
|
17207
|
+
const [error, setError] = useState(null);
|
|
17208
|
+
const isAbsentProvider = (err) => {
|
|
17209
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17210
|
+
return msg.includes("provider not available") || msg.includes("no 'ptz' binding");
|
|
17211
|
+
};
|
|
17212
|
+
const refreshPresets = useCallback(async () => {
|
|
17213
|
+
if (!enabled || !ptz) return;
|
|
17214
|
+
try {
|
|
17215
|
+
setPresets(await ptz.getPresets({}));
|
|
17216
|
+
} catch (err) {
|
|
17217
|
+
if (isAbsentProvider(err)) return;
|
|
17218
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17219
|
+
}
|
|
17220
|
+
}, [ptz, enabled]);
|
|
17221
|
+
const refreshOptions = useCallback(async () => {
|
|
17222
|
+
if (!enabled || !ptz) return;
|
|
17223
|
+
try {
|
|
17224
|
+
setOptions(await ptz.getOptions({}));
|
|
17225
|
+
} catch (err) {
|
|
17226
|
+
if (isAbsentProvider(err)) return;
|
|
17227
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17228
|
+
}
|
|
17229
|
+
}, [ptz, enabled]);
|
|
17101
17230
|
useEffect(() => {
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
}, [
|
|
17105
|
-
const
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17231
|
+
refreshPresets();
|
|
17232
|
+
refreshOptions();
|
|
17233
|
+
}, [refreshPresets, refreshOptions]);
|
|
17234
|
+
const wrap = useCallback(async (fn) => {
|
|
17235
|
+
if (!enabled || !ptz) return void 0;
|
|
17236
|
+
setError(null);
|
|
17237
|
+
setBusy(true);
|
|
17238
|
+
try {
|
|
17239
|
+
return await fn();
|
|
17240
|
+
} catch (err) {
|
|
17241
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17109
17242
|
return;
|
|
17243
|
+
} finally {
|
|
17244
|
+
setBusy(false);
|
|
17110
17245
|
}
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17246
|
+
}, [enabled, ptz]);
|
|
17247
|
+
return {
|
|
17248
|
+
move: useCallback(async (direction, speed) => {
|
|
17249
|
+
if (!ptz) return;
|
|
17250
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17251
|
+
const s = speed ?? defaultSpeed;
|
|
17252
|
+
await wrap(async () => {
|
|
17253
|
+
await ptz.move({
|
|
17254
|
+
pan: v.pan,
|
|
17255
|
+
tilt: v.tilt,
|
|
17256
|
+
speed: s
|
|
17257
|
+
});
|
|
17258
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17259
|
+
await ptz.stop({});
|
|
17260
|
+
});
|
|
17261
|
+
}, [
|
|
17262
|
+
ptz,
|
|
17263
|
+
defaultSpeed,
|
|
17264
|
+
pulseMs,
|
|
17265
|
+
wrap
|
|
17266
|
+
]),
|
|
17267
|
+
startContinuous: useCallback(async (direction, speed) => {
|
|
17268
|
+
if (!ptz) return;
|
|
17269
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17270
|
+
const s = speed ?? defaultSpeed;
|
|
17271
|
+
await wrap(() => ptz.continuousMove({
|
|
17272
|
+
pan: v.pan,
|
|
17273
|
+
tilt: v.tilt,
|
|
17274
|
+
speed: s
|
|
17275
|
+
}));
|
|
17276
|
+
}, [
|
|
17277
|
+
ptz,
|
|
17278
|
+
defaultSpeed,
|
|
17279
|
+
wrap
|
|
17280
|
+
]),
|
|
17281
|
+
stopContinuous: useCallback(async () => {
|
|
17282
|
+
if (!ptz) return;
|
|
17283
|
+
await wrap(() => ptz.stop({}));
|
|
17284
|
+
}, [ptz, wrap]),
|
|
17285
|
+
zoom: useCallback(async (direction, speed) => {
|
|
17286
|
+
if (!ptz) return;
|
|
17287
|
+
const z = direction === "in" ? 1 : -1;
|
|
17288
|
+
const s = speed ?? defaultSpeed;
|
|
17289
|
+
await wrap(async () => {
|
|
17290
|
+
await ptz.move({
|
|
17291
|
+
zoom: z,
|
|
17292
|
+
speed: s
|
|
17293
|
+
});
|
|
17294
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17295
|
+
await ptz.stop({});
|
|
17296
|
+
});
|
|
17297
|
+
}, [
|
|
17298
|
+
ptz,
|
|
17299
|
+
defaultSpeed,
|
|
17300
|
+
pulseMs,
|
|
17301
|
+
wrap
|
|
17302
|
+
]),
|
|
17303
|
+
goHome: useCallback(async () => {
|
|
17304
|
+
if (!ptz) return;
|
|
17305
|
+
await wrap(() => ptz.goHome({}));
|
|
17306
|
+
}, [ptz, wrap]),
|
|
17307
|
+
goToPreset: useCallback(async (presetId) => {
|
|
17308
|
+
if (!ptz) return;
|
|
17309
|
+
await wrap(() => ptz.goToPreset({ presetId }));
|
|
17310
|
+
}, [ptz, wrap]),
|
|
17311
|
+
savePreset: useCallback(async (name) => {
|
|
17312
|
+
if (!ptz) return void 0;
|
|
17313
|
+
return wrap(async () => {
|
|
17314
|
+
const usedIds = new Set(presets.map((p) => Number(p.id)).filter((n) => Number.isInteger(n) && n >= 0));
|
|
17315
|
+
let nextId = 0;
|
|
17316
|
+
while (usedIds.has(nextId)) nextId += 1;
|
|
17317
|
+
const presetId = String(nextId);
|
|
17318
|
+
await ptz.savePreset({
|
|
17319
|
+
presetId,
|
|
17320
|
+
name
|
|
17321
|
+
});
|
|
17322
|
+
await refreshPresets();
|
|
17323
|
+
return presetId;
|
|
17324
|
+
});
|
|
17325
|
+
}, [
|
|
17326
|
+
ptz,
|
|
17327
|
+
presets,
|
|
17328
|
+
refreshPresets,
|
|
17329
|
+
wrap
|
|
17330
|
+
]),
|
|
17331
|
+
deletePreset: useCallback(async (presetId) => {
|
|
17332
|
+
if (!ptz) return;
|
|
17333
|
+
await wrap(async () => {
|
|
17334
|
+
await ptz.deletePreset({ presetId });
|
|
17335
|
+
await refreshPresets();
|
|
17336
|
+
});
|
|
17337
|
+
}, [
|
|
17338
|
+
ptz,
|
|
17339
|
+
refreshPresets,
|
|
17340
|
+
wrap
|
|
17341
|
+
]),
|
|
17342
|
+
presets,
|
|
17343
|
+
refreshPresets,
|
|
17344
|
+
options,
|
|
17345
|
+
busy,
|
|
17346
|
+
error
|
|
17347
|
+
};
|
|
17348
|
+
}
|
|
17349
|
+
//#endregion
|
|
17350
|
+
//#region src/composites/ptz-overlay.tsx
|
|
17351
|
+
/**
|
|
17352
|
+
* PTZOverlay — pan / tilt / zoom controls.
|
|
17353
|
+
*
|
|
17354
|
+
* Two visual variants driven by `mode`:
|
|
17355
|
+
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
17356
|
+
* right of the camera viewport. Used as `extraOverlay` on the
|
|
17357
|
+
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
17358
|
+
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
17359
|
+
* legible against any frame.
|
|
17360
|
+
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
17361
|
+
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
17362
|
+
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
17363
|
+
* surrounding `bg-surface` so the dark theme reads consistently
|
|
17364
|
+
* instead of the previous always-dark-bubble look.
|
|
17365
|
+
*
|
|
17366
|
+
* Interaction model is identical across modes: short tap fires a
|
|
17367
|
+
* discrete pulse (`move`); long press starts continuous motion until
|
|
17368
|
+
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
17369
|
+
*/
|
|
17370
|
+
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
17371
|
+
const [pressedAt, setPressedAt] = useState(null);
|
|
17372
|
+
const [continuous, setContinuous] = useState(false);
|
|
17373
|
+
const handlePointerDown = useCallback((e) => {
|
|
17374
|
+
if (disabled) return;
|
|
17375
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17376
|
+
setPressedAt(Date.now());
|
|
17377
|
+
setContinuous(false);
|
|
17378
|
+
const timer = setTimeout(() => {
|
|
17379
|
+
setContinuous(true);
|
|
17380
|
+
onStart(direction);
|
|
17381
|
+
}, 250);
|
|
17382
|
+
e.currentTarget.dataset.timer = String(timer);
|
|
17383
|
+
}, [
|
|
17384
|
+
direction,
|
|
17385
|
+
disabled,
|
|
17386
|
+
onStart
|
|
17387
|
+
]);
|
|
17388
|
+
const handlePointerUp = useCallback((e) => {
|
|
17389
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17390
|
+
if (timerId) clearTimeout(timerId);
|
|
17391
|
+
e.currentTarget.dataset.timer = "";
|
|
17392
|
+
if (continuous) onStop();
|
|
17393
|
+
else if (pressedAt !== null) onMove(direction);
|
|
17394
|
+
setPressedAt(null);
|
|
17395
|
+
setContinuous(false);
|
|
17396
|
+
}, [
|
|
17397
|
+
continuous,
|
|
17398
|
+
direction,
|
|
17399
|
+
onMove,
|
|
17400
|
+
onStop,
|
|
17401
|
+
pressedAt
|
|
17402
|
+
]);
|
|
17403
|
+
const handlePointerCancel = useCallback((e) => {
|
|
17404
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17405
|
+
if (timerId) clearTimeout(timerId);
|
|
17406
|
+
e.currentTarget.dataset.timer = "";
|
|
17407
|
+
if (continuous) onStop();
|
|
17408
|
+
setPressedAt(null);
|
|
17409
|
+
setContinuous(false);
|
|
17410
|
+
}, [continuous, onStop]);
|
|
17411
|
+
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
17412
|
+
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17413
|
+
return /* @__PURE__ */ jsx("button", {
|
|
17414
|
+
type: "button",
|
|
17415
|
+
disabled,
|
|
17416
|
+
onPointerDown: handlePointerDown,
|
|
17417
|
+
onPointerUp: handlePointerUp,
|
|
17418
|
+
onPointerCancel: handlePointerCancel,
|
|
17419
|
+
className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
|
|
17420
|
+
title: direction,
|
|
17421
|
+
children: /* @__PURE__ */ jsx(Icon, { className: iconSizeClass })
|
|
17422
|
+
});
|
|
17423
|
+
}
|
|
17424
|
+
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
17425
|
+
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, savePreset, deletePreset, presets, options, busy, error } = controls;
|
|
17426
|
+
const confirm = useConfirm();
|
|
17427
|
+
const [presetsOpen, setPresetsOpen] = useState(false);
|
|
17428
|
+
const [presetName, setPresetName] = useState("");
|
|
17429
|
+
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
17430
|
+
const isPanel = mode === "panel";
|
|
17431
|
+
const presetManagementVisible = isPanel && (options?.supportsPresets ?? false);
|
|
17432
|
+
const maxPresetsReached = options?.maxPresets !== void 0 && presets.length >= options.maxPresets;
|
|
17433
|
+
const handleSavePreset = useCallback(async () => {
|
|
17434
|
+
const name = presetName.trim();
|
|
17435
|
+
if (!name || busy || maxPresetsReached) return;
|
|
17436
|
+
if (await savePreset(name) !== void 0) setPresetName("");
|
|
17437
|
+
}, [
|
|
17438
|
+
presetName,
|
|
17439
|
+
busy,
|
|
17440
|
+
maxPresetsReached,
|
|
17441
|
+
savePreset
|
|
17442
|
+
]);
|
|
17443
|
+
const handleDeletePreset = useCallback(async (presetId, label) => {
|
|
17444
|
+
if (!await confirm({
|
|
17445
|
+
title: "Delete preset",
|
|
17446
|
+
message: `Delete PTZ preset "${label}"? This removes it from the camera.`,
|
|
17447
|
+
confirmLabel: "Delete",
|
|
17448
|
+
variant: "danger"
|
|
17449
|
+
})) return;
|
|
17450
|
+
await deletePreset(presetId);
|
|
17451
|
+
}, [confirm, deletePreset]);
|
|
17452
|
+
const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
|
|
17453
|
+
const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
|
|
17454
|
+
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
17455
|
+
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17456
|
+
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
17457
|
+
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
17458
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17459
|
+
className: cn(containerClass, className),
|
|
17460
|
+
children: [
|
|
17461
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
17462
|
+
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
17463
|
+
children: ["PTZ: ", error]
|
|
17464
|
+
}),
|
|
17465
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17466
|
+
className: cn(rowClass, isPanel && "justify-center"),
|
|
17467
|
+
children: [
|
|
17468
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17469
|
+
className: "grid grid-cols-3 gap-0.5",
|
|
17470
|
+
children: [
|
|
17471
|
+
/* @__PURE__ */ jsx("span", {
|
|
17472
|
+
"aria-hidden": true,
|
|
17473
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17474
|
+
}),
|
|
17475
|
+
/* @__PURE__ */ jsx(DPadButton, {
|
|
17476
|
+
direction: "up",
|
|
17477
|
+
icon: ArrowUp,
|
|
17478
|
+
variant: mode,
|
|
17479
|
+
onMove: move,
|
|
17480
|
+
onStart: startContinuous,
|
|
17481
|
+
onStop: stopContinuous
|
|
17482
|
+
}),
|
|
17483
|
+
/* @__PURE__ */ jsx("span", {
|
|
17484
|
+
"aria-hidden": true,
|
|
17485
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17486
|
+
}),
|
|
17487
|
+
/* @__PURE__ */ jsx(DPadButton, {
|
|
17488
|
+
direction: "left",
|
|
17489
|
+
icon: ArrowLeft,
|
|
17490
|
+
variant: mode,
|
|
17491
|
+
onMove: move,
|
|
17492
|
+
onStart: startContinuous,
|
|
17493
|
+
onStop: stopContinuous
|
|
17494
|
+
}),
|
|
17495
|
+
/* @__PURE__ */ jsx("span", {
|
|
17496
|
+
"aria-hidden": true,
|
|
17497
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17498
|
+
}),
|
|
17499
|
+
/* @__PURE__ */ jsx(DPadButton, {
|
|
17500
|
+
direction: "right",
|
|
17501
|
+
icon: ArrowRight,
|
|
17502
|
+
variant: mode,
|
|
17503
|
+
onMove: move,
|
|
17504
|
+
onStart: startContinuous,
|
|
17505
|
+
onStop: stopContinuous
|
|
17506
|
+
}),
|
|
17507
|
+
/* @__PURE__ */ jsx("span", {
|
|
17508
|
+
"aria-hidden": true,
|
|
17509
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17510
|
+
}),
|
|
17511
|
+
/* @__PURE__ */ jsx(DPadButton, {
|
|
17512
|
+
direction: "down",
|
|
17513
|
+
icon: ArrowDown,
|
|
17514
|
+
variant: mode,
|
|
17515
|
+
onMove: move,
|
|
17516
|
+
onStart: startContinuous,
|
|
17517
|
+
onStop: stopContinuous
|
|
17518
|
+
}),
|
|
17519
|
+
/* @__PURE__ */ jsx("span", {
|
|
17520
|
+
"aria-hidden": true,
|
|
17521
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17522
|
+
})
|
|
17523
|
+
]
|
|
17524
|
+
}),
|
|
17525
|
+
(showZoom || showHome) && /* @__PURE__ */ jsx("div", { className: sepClass }),
|
|
17526
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17527
|
+
className: "flex flex-col gap-0.5",
|
|
17528
|
+
children: [showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
17529
|
+
type: "button",
|
|
17530
|
+
onClick: () => zoom("in"),
|
|
17531
|
+
disabled: busy,
|
|
17532
|
+
title: "Zoom in",
|
|
17533
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17534
|
+
children: /* @__PURE__ */ jsx(ZoomIn, { className: sideIconSize })
|
|
17535
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
17536
|
+
type: "button",
|
|
17537
|
+
onClick: () => zoom("out"),
|
|
17538
|
+
disabled: busy,
|
|
17539
|
+
title: "Zoom out",
|
|
17540
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17541
|
+
children: /* @__PURE__ */ jsx(ZoomOut, { className: sideIconSize })
|
|
17542
|
+
})] }), showHome && /* @__PURE__ */ jsx("button", {
|
|
17543
|
+
type: "button",
|
|
17544
|
+
onClick: () => goHome(),
|
|
17545
|
+
disabled: busy,
|
|
17546
|
+
title: "Go home",
|
|
17547
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17548
|
+
children: /* @__PURE__ */ jsx(House, { className: sideIconSize })
|
|
17549
|
+
})]
|
|
17550
|
+
}),
|
|
17551
|
+
presetsVisible && /* @__PURE__ */ jsxs("div", {
|
|
17552
|
+
className: "relative",
|
|
17553
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
17554
|
+
type: "button",
|
|
17555
|
+
onClick: () => setPresetsOpen((v) => !v),
|
|
17556
|
+
disabled: busy,
|
|
17557
|
+
className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
|
|
17558
|
+
title: "Presets",
|
|
17559
|
+
children: ["Presets", /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
17560
|
+
}), presetsOpen && /* @__PURE__ */ jsx("div", {
|
|
17561
|
+
className: cn("absolute right-0 top-full mt-1 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bg-surface border border-border" : "border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
|
|
17562
|
+
children: presets.map((p) => /* @__PURE__ */ jsxs("div", {
|
|
17563
|
+
className: cn("group flex items-center", isPanel ? "hover:bg-surface-hover" : "hover:bg-white/15"),
|
|
17564
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
17565
|
+
type: "button",
|
|
17566
|
+
onClick: () => {
|
|
17567
|
+
goToPreset(p.id);
|
|
17568
|
+
setPresetsOpen(false);
|
|
17569
|
+
},
|
|
17570
|
+
disabled: busy,
|
|
17571
|
+
className: cn("flex-1 px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground" : "text-white"),
|
|
17572
|
+
children: p.name || p.id
|
|
17573
|
+
}), presetManagementVisible && /* @__PURE__ */ jsx("button", {
|
|
17574
|
+
type: "button",
|
|
17575
|
+
onClick: () => void handleDeletePreset(p.id, p.name || p.id),
|
|
17576
|
+
disabled: busy,
|
|
17577
|
+
title: `Delete preset ${p.name || p.id}`,
|
|
17578
|
+
className: cn("mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded", "text-foreground-subtle hover:bg-danger/15 hover:text-danger disabled:opacity-40"),
|
|
17579
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
17580
|
+
})]
|
|
17581
|
+
}, p.id))
|
|
17582
|
+
})]
|
|
17583
|
+
})
|
|
17584
|
+
]
|
|
17585
|
+
}),
|
|
17586
|
+
presetManagementVisible && /* @__PURE__ */ jsxs("div", {
|
|
17587
|
+
className: "flex flex-col gap-1 rounded-lg border border-border p-2",
|
|
17588
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
17589
|
+
className: "flex items-center gap-2",
|
|
17590
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
17591
|
+
type: "text",
|
|
17592
|
+
value: presetName,
|
|
17593
|
+
onChange: (e) => setPresetName(e.target.value),
|
|
17594
|
+
onKeyDown: (e) => {
|
|
17595
|
+
if (e.key === "Enter") handleSavePreset();
|
|
17596
|
+
},
|
|
17597
|
+
disabled: busy || maxPresetsReached,
|
|
17598
|
+
placeholder: "New preset name",
|
|
17599
|
+
maxLength: 64,
|
|
17600
|
+
className: cn("min-w-0 flex-1 rounded border border-border bg-surface px-2 py-1 text-[11px] text-foreground", "placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-primary/50", "disabled:opacity-40")
|
|
17601
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17602
|
+
type: "button",
|
|
17603
|
+
onClick: () => void handleSavePreset(),
|
|
17604
|
+
disabled: busy || maxPresetsReached || presetName.trim().length === 0,
|
|
17605
|
+
title: maxPresetsReached ? `Camera preset limit reached (${options?.maxPresets})` : "Save current position as a new preset",
|
|
17606
|
+
className: cn("flex h-7 shrink-0 items-center gap-1 rounded px-2 text-[10px] font-medium", "bg-primary/15 text-primary hover:bg-primary/25 disabled:opacity-40"),
|
|
17607
|
+
children: [/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }), "Save current position"]
|
|
17608
|
+
})]
|
|
17609
|
+
}), maxPresetsReached && /* @__PURE__ */ jsxs("span", {
|
|
17610
|
+
className: "text-[10px] text-foreground-subtle",
|
|
17611
|
+
children: [
|
|
17612
|
+
"Camera preset limit reached (",
|
|
17613
|
+
options?.maxPresets,
|
|
17614
|
+
"). Delete a preset to add a new one."
|
|
17615
|
+
]
|
|
17616
|
+
})]
|
|
17617
|
+
})
|
|
17618
|
+
]
|
|
17619
|
+
});
|
|
17620
|
+
}
|
|
17621
|
+
//#endregion
|
|
17622
|
+
//#region src/composites/cap-settings/PtzPanel.tsx
|
|
17623
|
+
/**
|
|
17624
|
+
* PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
|
|
17625
|
+
* cap-settings component (ui-library), mounted as a cap-contributed
|
|
17626
|
+
* top-level device-detail tab via the cap-UI contribution mechanism
|
|
17627
|
+
* (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
|
|
17628
|
+
*
|
|
17629
|
+
* Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
|
|
17630
|
+
* the controls, `PTZOverlay` renders them (`mode='panel'`), and an
|
|
17631
|
+
* Autofocus toggle is shown only when `getOptions().hasAutofocus`.
|
|
17632
|
+
* Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
|
|
17633
|
+
* independently by the contribution mechanism — not nested here.
|
|
17634
|
+
*/
|
|
17635
|
+
/** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
|
|
17636
|
+
* `getStatus().autofocus` and drives `setAutofocus`. */
|
|
17637
|
+
function AutofocusToggle({ deviceId }) {
|
|
17638
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
17639
|
+
const [enabled, setEnabled] = useState(null);
|
|
17640
|
+
const [busy, setBusy] = useState(false);
|
|
17641
|
+
useEffect(() => {
|
|
17642
|
+
if (!dev) return void 0;
|
|
17643
|
+
let cancelled = false;
|
|
17644
|
+
(async () => {
|
|
17645
|
+
try {
|
|
17646
|
+
const status = await dev.ptz?.getStatus({});
|
|
17647
|
+
if (cancelled || !status) return;
|
|
17648
|
+
setEnabled(status.autofocus);
|
|
17649
|
+
} catch {}
|
|
17650
|
+
})();
|
|
17651
|
+
return () => {
|
|
17652
|
+
cancelled = true;
|
|
17653
|
+
};
|
|
17654
|
+
}, [dev]);
|
|
17655
|
+
const toggle = useCallback(async () => {
|
|
17656
|
+
if (!dev?.ptz || enabled === null) return;
|
|
17657
|
+
setBusy(true);
|
|
17658
|
+
try {
|
|
17659
|
+
const next = !enabled;
|
|
17660
|
+
await dev.ptz.setAutofocus({ enabled: next });
|
|
17661
|
+
setEnabled(next);
|
|
17662
|
+
} catch (err) {
|
|
17663
|
+
console.error("ptz.setAutofocus failed", err);
|
|
17664
|
+
} finally {
|
|
17665
|
+
setBusy(false);
|
|
17666
|
+
}
|
|
17667
|
+
}, [dev, enabled]);
|
|
17668
|
+
if (enabled === null) return null;
|
|
17669
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17670
|
+
className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
|
|
17671
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17672
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
17673
|
+
children: "Autofocus"
|
|
17674
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17675
|
+
type: "button",
|
|
17676
|
+
onClick: () => void toggle(),
|
|
17677
|
+
disabled: busy,
|
|
17678
|
+
"aria-pressed": enabled,
|
|
17679
|
+
className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
|
|
17680
|
+
children: [/* @__PURE__ */ jsx("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
|
|
17681
|
+
})]
|
|
17682
|
+
});
|
|
17683
|
+
}
|
|
17684
|
+
function PtzPanel({ deviceId }) {
|
|
17685
|
+
const system = useSystem();
|
|
17686
|
+
const ready = Number.isFinite(deviceId);
|
|
17687
|
+
const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
|
|
17688
|
+
if (!ready) return /* @__PURE__ */ jsxs("div", {
|
|
17689
|
+
className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
|
|
17690
|
+
children: [
|
|
17691
|
+
"Device #",
|
|
17692
|
+
deviceId,
|
|
17693
|
+
" not loaded."
|
|
17694
|
+
]
|
|
17695
|
+
});
|
|
17696
|
+
const hasAutofocus = controls.options?.hasAutofocus === true;
|
|
17697
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17698
|
+
className: "flex flex-col h-full overflow-y-auto",
|
|
17699
|
+
children: [
|
|
17700
|
+
/* @__PURE__ */ jsx("h3", {
|
|
17701
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2",
|
|
17702
|
+
children: "PTZ"
|
|
17703
|
+
}),
|
|
17704
|
+
/* @__PURE__ */ jsx(PTZOverlay, {
|
|
17705
|
+
controls,
|
|
17706
|
+
mode: "panel"
|
|
17707
|
+
}),
|
|
17708
|
+
hasAutofocus && /* @__PURE__ */ jsx(AutofocusToggle, { deviceId })
|
|
17709
|
+
]
|
|
17710
|
+
});
|
|
17711
|
+
}
|
|
17712
|
+
//#endregion
|
|
17713
|
+
//#region src/hooks/use-device-autotrack.ts
|
|
17714
|
+
/**
|
|
17715
|
+
* `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
|
|
17716
|
+
*
|
|
17717
|
+
* Surface:
|
|
17718
|
+
* - `status` — current `{enabled, lastChangedAt, currentSettings}`,
|
|
17719
|
+
* polled at 5s intervals while the hook is mounted.
|
|
17720
|
+
* - `setEnabled(on)` — flip the on/off state.
|
|
17721
|
+
* - `setSettings(partial)` — patch one or more settings keys.
|
|
17722
|
+
* - `isPending` — true while a mutation is in flight.
|
|
17723
|
+
*
|
|
17724
|
+
* Returns `null` for `status` when the cap isn't available (device
|
|
17725
|
+
* doesn't support autotrack) — callers gate render on this OR on the
|
|
17726
|
+
* device's `PtzAutotrack` feature flag.
|
|
17727
|
+
*/
|
|
17728
|
+
function useDeviceAutotrack(deviceId) {
|
|
17729
|
+
const queryClient = useQueryClient();
|
|
17730
|
+
const statusQuery = usePtzAutotrackGetStatus({ deviceId: deviceId ?? 0 }, {
|
|
17731
|
+
enabled: deviceId !== null && Number.isFinite(deviceId),
|
|
17732
|
+
refetchInterval: 5e3,
|
|
17733
|
+
retry: 1
|
|
17734
|
+
});
|
|
17735
|
+
const setEnabledMutation = usePtzAutotrackSetEnabled({ onSuccess: () => {
|
|
17736
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17737
|
+
} });
|
|
17738
|
+
const setSettingsMutation = usePtzAutotrackSetSettings({ onSuccess: () => {
|
|
17739
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17740
|
+
} });
|
|
17741
|
+
const setEnabled = useCallback(async (on) => {
|
|
17742
|
+
if (deviceId === null) return;
|
|
17743
|
+
await setEnabledMutation.mutateAsync({
|
|
17744
|
+
deviceId,
|
|
17745
|
+
enabled: on
|
|
17746
|
+
});
|
|
17747
|
+
}, [deviceId, setEnabledMutation]);
|
|
17748
|
+
const setSettings = useCallback(async (patch) => {
|
|
17749
|
+
if (deviceId === null) return;
|
|
17750
|
+
await setSettingsMutation.mutateAsync({
|
|
17751
|
+
deviceId,
|
|
17752
|
+
settings: patch
|
|
17753
|
+
});
|
|
17754
|
+
}, [deviceId, setSettingsMutation]);
|
|
17755
|
+
const errorMsg = (() => {
|
|
17756
|
+
if (statusQuery.error) return statusQuery.error.message;
|
|
17757
|
+
if (setEnabledMutation.error) return setEnabledMutation.error.message;
|
|
17758
|
+
if (setSettingsMutation.error) return setSettingsMutation.error.message;
|
|
17759
|
+
return null;
|
|
17760
|
+
})();
|
|
17761
|
+
return {
|
|
17762
|
+
status: statusQuery.data ?? null,
|
|
17763
|
+
isLoading: statusQuery.isLoading,
|
|
17764
|
+
isPending: setEnabledMutation.isPending || setSettingsMutation.isPending,
|
|
17765
|
+
error: errorMsg,
|
|
17766
|
+
setEnabled,
|
|
17767
|
+
setSettings
|
|
17768
|
+
};
|
|
17769
|
+
}
|
|
17770
|
+
//#endregion
|
|
17771
|
+
//#region src/composites/cap-settings/AutotrackSection.tsx
|
|
17772
|
+
/**
|
|
17773
|
+
* Autotrack settings card — rendered inside the PTZ panel when the
|
|
17774
|
+
* device has `DeviceFeature.PtzAutotrack`. Three knobs (cross-vendor
|
|
17775
|
+
* schema): target type, stop delay, disappear delay, plus an enable /
|
|
17776
|
+
* disable toggle that drives the `ptz-autotrack` cap directly.
|
|
17777
|
+
*
|
|
17778
|
+
* Save semantics:
|
|
17779
|
+
* - Toggle (enable/disable) calls `setEnabled` immediately.
|
|
17780
|
+
* - Form fields debounce-save on blur — no explicit Save button.
|
|
17781
|
+
*
|
|
17782
|
+
* Vendor capability hints:
|
|
17783
|
+
* - The cap's status carries `currentSettings` from the camera's
|
|
17784
|
+
* own GET; rendered on every poll so the operator sees what's
|
|
17785
|
+
* really applied vs what they typed.
|
|
17786
|
+
* - When the firmware ignored a setting, the field stays editable.
|
|
17787
|
+
*/
|
|
17788
|
+
function AutotrackSection({ deviceId }) {
|
|
17789
|
+
const { status, isLoading, isPending, error, setEnabled, setSettings } = useDeviceAutotrack(Number.isFinite(deviceId) ? deviceId : null);
|
|
17790
|
+
const [draft, setDraft] = useState({
|
|
17791
|
+
targetType: "",
|
|
17792
|
+
stopDelaySeconds: 30,
|
|
17793
|
+
disappearDelaySeconds: 15
|
|
17794
|
+
});
|
|
17795
|
+
const [editing, setEditing] = useState(false);
|
|
17796
|
+
useEffect(() => {
|
|
17797
|
+
if (editing) return;
|
|
17798
|
+
const s = status?.currentSettings;
|
|
17799
|
+
if (!s) return;
|
|
17800
|
+
setDraft({
|
|
17801
|
+
targetType: s.targetType,
|
|
17802
|
+
stopDelaySeconds: s.stopDelaySeconds,
|
|
17803
|
+
disappearDelaySeconds: s.disappearDelaySeconds
|
|
17804
|
+
});
|
|
17805
|
+
}, [status, editing]);
|
|
17806
|
+
if (isLoading && !status) return /* @__PURE__ */ jsxs("div", {
|
|
17807
|
+
className: "flex items-center gap-2 px-3 py-2 text-[10.5px] text-foreground-subtle",
|
|
17808
|
+
children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3 w-3 animate-spin" }), "Loading autotrack…"]
|
|
17809
|
+
});
|
|
17810
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
17811
|
+
className: "border-t border-border/40 mt-2 pt-2 px-1 space-y-2",
|
|
17812
|
+
children: [
|
|
17813
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17814
|
+
className: "flex items-center justify-between px-2",
|
|
17815
|
+
children: [/* @__PURE__ */ jsxs("h3", {
|
|
17816
|
+
className: "flex items-center gap-1.5 text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
17817
|
+
children: [/* @__PURE__ */ jsx(Crosshair, { className: "h-3 w-3 text-primary" }), "Autotrack"]
|
|
17818
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
17819
|
+
type: "button",
|
|
17820
|
+
onClick: () => {
|
|
17821
|
+
setEnabled(!(status?.enabled ?? false));
|
|
17822
|
+
},
|
|
17823
|
+
disabled: isPending,
|
|
17824
|
+
className: `flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${status?.enabled ? "bg-success/15 text-success hover:bg-success/25" : "bg-surface-elevated/40 text-foreground-subtle hover:bg-surface-hover hover:text-foreground"} disabled:opacity-50`,
|
|
17825
|
+
"aria-pressed": status?.enabled ?? false,
|
|
17826
|
+
children: [/* @__PURE__ */ jsx("span", { className: `h-1.5 w-1.5 rounded-full ${status?.enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), status?.enabled ? "Enabled" : "Disabled"]
|
|
17827
|
+
})]
|
|
17828
|
+
}),
|
|
17829
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
17830
|
+
className: "flex items-start gap-1.5 rounded-md border border-danger/30 bg-danger/5 px-2 py-1.5 text-[10px] text-danger",
|
|
17831
|
+
children: [/* @__PURE__ */ jsx(CircleAlert, { className: "h-3 w-3 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("span", {
|
|
17832
|
+
className: "leading-snug break-words",
|
|
17833
|
+
children: error
|
|
17834
|
+
})]
|
|
17835
|
+
}),
|
|
17836
|
+
/* @__PURE__ */ jsxs("div", {
|
|
17837
|
+
className: "grid grid-cols-2 gap-2 px-2",
|
|
17838
|
+
children: [
|
|
17839
|
+
(status?.supportedTargetTypes?.length ?? 0) > 0 && /* @__PURE__ */ jsxs("label", {
|
|
17840
|
+
className: "col-span-2 flex flex-col gap-1",
|
|
17841
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17842
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17843
|
+
children: "Target type"
|
|
17844
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
17845
|
+
value: draft.targetType,
|
|
17846
|
+
onFocus: () => setEditing(true),
|
|
17847
|
+
onChange: (e) => setDraft((d) => ({
|
|
17848
|
+
...d,
|
|
17849
|
+
targetType: e.target.value
|
|
17850
|
+
})),
|
|
17851
|
+
onBlur: () => {
|
|
17852
|
+
setEditing(false);
|
|
17853
|
+
const current = status?.currentSettings?.targetType ?? "";
|
|
17854
|
+
if (draft.targetType !== current) setSettings({ targetType: draft.targetType });
|
|
17855
|
+
},
|
|
17856
|
+
disabled: isPending,
|
|
17857
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50",
|
|
17858
|
+
children: (status?.supportedTargetTypes ?? []).map((opt) => /* @__PURE__ */ jsx("option", {
|
|
17859
|
+
value: opt.value,
|
|
17860
|
+
children: opt.label
|
|
17861
|
+
}, opt.value))
|
|
17862
|
+
})]
|
|
17863
|
+
}),
|
|
17864
|
+
/* @__PURE__ */ jsxs("label", {
|
|
17865
|
+
className: "flex flex-col gap-1",
|
|
17866
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17867
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17868
|
+
children: "Stop delay (s)"
|
|
17869
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
17870
|
+
type: "number",
|
|
17871
|
+
min: 0,
|
|
17872
|
+
max: 300,
|
|
17873
|
+
value: draft.stopDelaySeconds,
|
|
17874
|
+
onFocus: () => setEditing(true),
|
|
17875
|
+
onChange: (e) => setDraft((d) => ({
|
|
17876
|
+
...d,
|
|
17877
|
+
stopDelaySeconds: Number(e.target.value) || 0
|
|
17878
|
+
})),
|
|
17879
|
+
onBlur: () => {
|
|
17880
|
+
setEditing(false);
|
|
17881
|
+
const current = status?.currentSettings?.stopDelaySeconds ?? 30;
|
|
17882
|
+
if (draft.stopDelaySeconds !== current) setSettings({ stopDelaySeconds: draft.stopDelaySeconds });
|
|
17883
|
+
},
|
|
17884
|
+
disabled: isPending,
|
|
17885
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
|
|
17886
|
+
})]
|
|
17887
|
+
}),
|
|
17888
|
+
/* @__PURE__ */ jsxs("label", {
|
|
17889
|
+
className: "flex flex-col gap-1",
|
|
17890
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
17891
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17892
|
+
children: "Disappear delay (s)"
|
|
17893
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
17894
|
+
type: "number",
|
|
17895
|
+
min: 0,
|
|
17896
|
+
max: 300,
|
|
17897
|
+
value: draft.disappearDelaySeconds,
|
|
17898
|
+
onFocus: () => setEditing(true),
|
|
17899
|
+
onChange: (e) => setDraft((d) => ({
|
|
17900
|
+
...d,
|
|
17901
|
+
disappearDelaySeconds: Number(e.target.value) || 0
|
|
17902
|
+
})),
|
|
17903
|
+
onBlur: () => {
|
|
17904
|
+
setEditing(false);
|
|
17905
|
+
const current = status?.currentSettings?.disappearDelaySeconds ?? 15;
|
|
17906
|
+
if (draft.disappearDelaySeconds !== current) setSettings({ disappearDelaySeconds: draft.disappearDelaySeconds });
|
|
17907
|
+
},
|
|
17908
|
+
disabled: isPending,
|
|
17909
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:border-primary focus:outline-none disabled:opacity-50"
|
|
17910
|
+
})]
|
|
17911
|
+
})
|
|
17912
|
+
]
|
|
17913
|
+
}),
|
|
17914
|
+
/* @__PURE__ */ jsx("p", {
|
|
17915
|
+
className: "px-2 text-[9.5px] text-foreground-subtle leading-snug italic",
|
|
17916
|
+
children: "Settings save on blur. Vendor support varies — Reolink honours all three; Hikvision honours target type + stop delay only (disappear delay is accepted but ignored at firmware push)."
|
|
17917
|
+
})
|
|
17918
|
+
]
|
|
17919
|
+
});
|
|
17920
|
+
}
|
|
17921
|
+
//#endregion
|
|
17922
|
+
//#region src/contexts/player-overlays.tsx
|
|
17923
|
+
/**
|
|
17924
|
+
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
17925
|
+
* the device-detail live-frame `StreamPanel`.
|
|
17926
|
+
*
|
|
17927
|
+
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
17928
|
+
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
17929
|
+
* editing state by hand. Adding a new feature (audio waveform,
|
|
17930
|
+
* detection bbox tracker, recording controls, …) meant another
|
|
17931
|
+
* round of plumbing through the host file and StreamPanel's prop
|
|
17932
|
+
* surface. The registry lets a sibling component (Detection tab,
|
|
17933
|
+
* Live Stats tab, an addon-page extension) declare its own layer
|
|
17934
|
+
* imperatively via a hook, and the host renders whatever's
|
|
17935
|
+
* registered.
|
|
17936
|
+
*
|
|
17937
|
+
* Two registries live side-by-side:
|
|
17938
|
+
*
|
|
17939
|
+
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
17940
|
+
* video frame (zones polygon canvas, motion bbox overlay,
|
|
17941
|
+
* audio waveform). Rendered ordered by `order` (lower first
|
|
17942
|
+
* → bottom; higher → top); the last-registered layer wins
|
|
17943
|
+
* ties.
|
|
17944
|
+
*
|
|
17945
|
+
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
17946
|
+
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
17947
|
+
* Each carries an icon, label, controlled `active` flag, and
|
|
17948
|
+
* a click handler. The host (StreamPanel) renders them
|
|
17949
|
+
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
17950
|
+
*
|
|
17951
|
+
* Lifecycle: hooks register their layer / button on mount and
|
|
17952
|
+
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
17953
|
+
* automatically tears down the registration. Re-registering with
|
|
17954
|
+
* the same `id` replaces the prior entry — meaning a single hook
|
|
17955
|
+
* call can update its overlay/button props on every render without
|
|
17956
|
+
* leaking entries.
|
|
17957
|
+
*
|
|
17958
|
+
* Provider scope: typically wraps the whole device-detail subtree
|
|
17959
|
+
* so the StreamPanel + every tab share the same registry. One
|
|
17960
|
+
* provider per `deviceId`; switching device IDs unmounts the
|
|
17961
|
+
* provider naturally.
|
|
17962
|
+
*/
|
|
17963
|
+
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
17964
|
+
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
17965
|
+
function PlayerOverlaysProvider({ children }) {
|
|
17966
|
+
const [layers, setLayers] = useState(() => /* @__PURE__ */ new Map());
|
|
17967
|
+
const [buttons, setButtons] = useState(() => /* @__PURE__ */ new Map());
|
|
17968
|
+
const setLayer = useCallback((layer) => {
|
|
17969
|
+
setLayers((prev) => {
|
|
17970
|
+
const next = new Map(prev);
|
|
17971
|
+
next.set(layer.id, layer);
|
|
17972
|
+
return next;
|
|
17973
|
+
});
|
|
17974
|
+
}, []);
|
|
17975
|
+
const removeLayer = useCallback((id) => {
|
|
17976
|
+
setLayers((prev) => {
|
|
17977
|
+
if (!prev.has(id)) return prev;
|
|
17978
|
+
const next = new Map(prev);
|
|
17979
|
+
next.delete(id);
|
|
17980
|
+
return next;
|
|
17981
|
+
});
|
|
17982
|
+
}, []);
|
|
17983
|
+
const setButton = useCallback((button) => {
|
|
17984
|
+
setButtons((prev) => {
|
|
17985
|
+
const next = new Map(prev);
|
|
17986
|
+
next.set(button.id, button);
|
|
17987
|
+
return next;
|
|
17988
|
+
});
|
|
17989
|
+
}, []);
|
|
17990
|
+
const removeButton = useCallback((id) => {
|
|
17991
|
+
setButtons((prev) => {
|
|
17992
|
+
if (!prev.has(id)) return prev;
|
|
17993
|
+
const next = new Map(prev);
|
|
17994
|
+
next.delete(id);
|
|
17995
|
+
return next;
|
|
17996
|
+
});
|
|
17997
|
+
}, []);
|
|
17998
|
+
const stateValue = useMemo(() => ({
|
|
17999
|
+
layers,
|
|
18000
|
+
buttons
|
|
18001
|
+
}), [layers, buttons]);
|
|
18002
|
+
const actionsValue = useMemo(() => ({
|
|
18003
|
+
setLayer,
|
|
18004
|
+
removeLayer,
|
|
18005
|
+
setButton,
|
|
18006
|
+
removeButton
|
|
18007
|
+
}), [
|
|
18008
|
+
setLayer,
|
|
18009
|
+
removeLayer,
|
|
18010
|
+
setButton,
|
|
18011
|
+
removeButton
|
|
18012
|
+
]);
|
|
18013
|
+
return /* @__PURE__ */ jsx(PlayerOverlaysStateContext.Provider, {
|
|
18014
|
+
value: stateValue,
|
|
18015
|
+
children: /* @__PURE__ */ jsx(PlayerOverlaysActionsContext.Provider, {
|
|
18016
|
+
value: actionsValue,
|
|
18017
|
+
children
|
|
18018
|
+
})
|
|
18019
|
+
});
|
|
18020
|
+
}
|
|
18021
|
+
/** Snapshot of registered layers ordered by `order` (asc, ties resolved
|
|
18022
|
+
* by insertion order of the underlying Map). Returns `[]` outside a
|
|
18023
|
+
* provider. */
|
|
18024
|
+
function usePlayerOverlayLayers() {
|
|
18025
|
+
const state = useContext(PlayerOverlaysStateContext);
|
|
18026
|
+
return useMemo(() => {
|
|
18027
|
+
if (!state) return [];
|
|
18028
|
+
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
18029
|
+
}, [state]);
|
|
18030
|
+
}
|
|
18031
|
+
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
18032
|
+
function usePlayerToolbarButtons() {
|
|
18033
|
+
const state = useContext(PlayerOverlaysStateContext);
|
|
18034
|
+
return useMemo(() => {
|
|
18035
|
+
if (!state) return [];
|
|
18036
|
+
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
18037
|
+
}, [state]);
|
|
18038
|
+
}
|
|
18039
|
+
/**
|
|
18040
|
+
* Register an overlay layer for the lifetime of the calling component.
|
|
18041
|
+
* Re-registers on every render with the latest spec; auto-unregisters
|
|
18042
|
+
* on unmount. Pass `null` to skip registration when the layer is
|
|
18043
|
+
* conditionally enabled (the hook still runs every render — keeps
|
|
18044
|
+
* react-hook order stable across spec === null toggles).
|
|
18045
|
+
*
|
|
18046
|
+
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
18047
|
+
* to avoid re-registering on every parent render — context-write
|
|
18048
|
+
* effects depend on referential equality of the spec object.
|
|
18049
|
+
*/
|
|
18050
|
+
function usePlayerOverlayLayer(spec) {
|
|
18051
|
+
const actions = useContext(PlayerOverlaysActionsContext);
|
|
18052
|
+
useEffect(() => {
|
|
18053
|
+
if (!actions || !spec) return void 0;
|
|
18054
|
+
actions.setLayer(spec);
|
|
18055
|
+
return () => actions.removeLayer(spec.id);
|
|
18056
|
+
}, [actions, spec]);
|
|
18057
|
+
}
|
|
18058
|
+
/** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
|
|
18059
|
+
function usePlayerToolbarButton(spec) {
|
|
18060
|
+
const actions = useContext(PlayerOverlaysActionsContext);
|
|
18061
|
+
useEffect(() => {
|
|
18062
|
+
if (!actions || !spec) return void 0;
|
|
18063
|
+
actions.setButton(spec);
|
|
18064
|
+
return () => actions.removeButton(spec.id);
|
|
18065
|
+
}, [actions, spec]);
|
|
18066
|
+
}
|
|
18067
|
+
//#endregion
|
|
18068
|
+
//#region src/composites/cap-settings/MotionGridCanvas.tsx
|
|
18069
|
+
/**
|
|
18070
|
+
* MotionGridCanvas — the on-frame motion-zone grid editor.
|
|
18071
|
+
*
|
|
18072
|
+
* This component is *only* the editable lattice painted over the live
|
|
18073
|
+
* frame: a plain CSS-grid of `gridWidth × gridHeight` cells. It carries
|
|
18074
|
+
* NO controls — enabled / sensitivity / save / quick-actions all live
|
|
18075
|
+
* in the management bar inside the settings section (`MotionZonesTab`),
|
|
18076
|
+
* so the live frame stays unobstructed.
|
|
18077
|
+
*
|
|
18078
|
+
* Interaction:
|
|
18079
|
+
* - Click a cell → toggle it.
|
|
18080
|
+
* - Press + drag → paint every touched cell to whatever the
|
|
18081
|
+
* first-touched cell flipped TO (the standard grid-mask gesture).
|
|
18082
|
+
*
|
|
18083
|
+
* Purely controlled — every change is forwarded through `onCellsChange`;
|
|
18084
|
+
* the owning `MotionZonesTab` keeps the single source of truth.
|
|
18085
|
+
*/
|
|
18086
|
+
function MotionGridCanvas({ options, cells, onCellsChange }) {
|
|
18087
|
+
const { gridWidth, gridHeight } = options;
|
|
18088
|
+
const total = gridWidth * gridHeight;
|
|
18089
|
+
const paintingRef = useRef(false);
|
|
18090
|
+
const paintValueRef = useRef(true);
|
|
18091
|
+
const paintedRef = useRef(/* @__PURE__ */ new Set());
|
|
18092
|
+
const [, forceTick] = useState(0);
|
|
18093
|
+
const applyCell = useCallback((index, value) => {
|
|
18094
|
+
if (index < 0 || index >= total) return;
|
|
18095
|
+
if (cells[index] === value) return;
|
|
18096
|
+
const next = [...cells];
|
|
18097
|
+
while (next.length < total) next.push(false);
|
|
18098
|
+
next.length = total;
|
|
18099
|
+
next[index] = value;
|
|
18100
|
+
onCellsChange(next);
|
|
18101
|
+
}, [
|
|
18102
|
+
cells,
|
|
18103
|
+
total,
|
|
18104
|
+
onCellsChange
|
|
18105
|
+
]);
|
|
18106
|
+
const handlePointerDown = useCallback((index) => (e) => {
|
|
18107
|
+
e.preventDefault();
|
|
18108
|
+
const nextValue = !cells[index];
|
|
18109
|
+
paintingRef.current = true;
|
|
18110
|
+
paintValueRef.current = nextValue;
|
|
18111
|
+
paintedRef.current = new Set([index]);
|
|
18112
|
+
applyCell(index, nextValue);
|
|
18113
|
+
forceTick((t) => t + 1);
|
|
18114
|
+
}, [cells, applyCell]);
|
|
18115
|
+
const handlePointerEnter = useCallback((index) => () => {
|
|
18116
|
+
if (!paintingRef.current) return;
|
|
18117
|
+
if (paintedRef.current.has(index)) return;
|
|
18118
|
+
paintedRef.current.add(index);
|
|
18119
|
+
applyCell(index, paintValueRef.current);
|
|
18120
|
+
}, [applyCell]);
|
|
18121
|
+
const endPaint = useCallback(() => {
|
|
18122
|
+
if (!paintingRef.current) return;
|
|
18123
|
+
paintingRef.current = false;
|
|
18124
|
+
paintedRef.current = /* @__PURE__ */ new Set();
|
|
18125
|
+
}, []);
|
|
18126
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18127
|
+
className: "absolute inset-0 select-none grid",
|
|
18128
|
+
style: {
|
|
18129
|
+
gridTemplateColumns: `repeat(${String(gridWidth)}, 1fr)`,
|
|
18130
|
+
gridTemplateRows: `repeat(${String(gridHeight)}, 1fr)`
|
|
18131
|
+
},
|
|
18132
|
+
onPointerUp: endPaint,
|
|
18133
|
+
onPointerLeave: endPaint,
|
|
18134
|
+
children: Array.from({ length: total }, (_, i) => {
|
|
18135
|
+
const active = cells[i] === true;
|
|
18136
|
+
return /* @__PURE__ */ jsx("button", {
|
|
18137
|
+
type: "button",
|
|
18138
|
+
"aria-label": `motion cell ${String(i)}`,
|
|
18139
|
+
"aria-pressed": active,
|
|
18140
|
+
onPointerDown: handlePointerDown(i),
|
|
18141
|
+
onPointerEnter: handlePointerEnter(i),
|
|
18142
|
+
className: active ? "border border-primary/70 bg-primary/55 hover:bg-primary/65 transition-colors" : "border border-white/25 bg-white/10 hover:bg-white/20 transition-colors"
|
|
18143
|
+
}, i);
|
|
18144
|
+
})
|
|
18145
|
+
});
|
|
18146
|
+
}
|
|
18147
|
+
//#endregion
|
|
18148
|
+
//#region src/composites/cap-settings/MotionZonesSettings.tsx
|
|
18149
|
+
/**
|
|
18150
|
+
* MotionZonesSettings — management surface for the on-camera
|
|
18151
|
+
* `motion-zones` capability. A cap-settings component (ui-library),
|
|
18152
|
+
* mounted into the device-detail Config tab via the cap-UI
|
|
18153
|
+
* contribution mechanism (the `motion-zones` cap declares a `ui` block).
|
|
18154
|
+
*
|
|
18155
|
+
* Split of concerns (mirrors the analytics zones drawer):
|
|
18156
|
+
* - The editable GRID is painted over the live frame by
|
|
18157
|
+
* `MotionGridCanvas`, registered as a player-overlay layer only
|
|
18158
|
+
* while the operator has toggled "Edit grid" on (OFF by default).
|
|
18159
|
+
* - Every CONTROL lives here, in the settings section, NOT on the
|
|
18160
|
+
* frame: a quick-action bar (All on / All off / Invert), Save /
|
|
18161
|
+
* Revert, and the cell-count status.
|
|
18162
|
+
* - Detection enable + sensitivity are NOT edited here — they are
|
|
18163
|
+
* the camera's on-board motion master switch, owned by the
|
|
18164
|
+
* driver's "On-camera motion" settings section. The grid editor
|
|
18165
|
+
* only paints the mask (`setZone` with a `cells`-only patch;
|
|
18166
|
+
* enable / sensitivity are left untouched camera-side).
|
|
18167
|
+
*
|
|
18168
|
+
* The grid editor draws nothing on cameras without the `motion-zones`
|
|
18169
|
+
* cap — the fetch self-gates on a failed `getOptions`.
|
|
18170
|
+
*/
|
|
18171
|
+
/** "Camera doesn't expose the cap" — swallow so the editor stays
|
|
18172
|
+
* hidden on unsupported devices (mirrors `usePTZ`'s gate). */
|
|
18173
|
+
function isAbsentProvider(err) {
|
|
18174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18175
|
+
return msg.includes("provider not available") || msg.includes("motion-zones");
|
|
18176
|
+
}
|
|
18177
|
+
/** Coerce an arbitrary `cells` array to exactly `gridWidth*gridHeight`. */
|
|
18178
|
+
function normaliseCells(cells, options) {
|
|
18179
|
+
const total = options.gridWidth * options.gridHeight;
|
|
18180
|
+
const next = new Array(total);
|
|
18181
|
+
for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
|
|
18182
|
+
return next;
|
|
18183
|
+
}
|
|
18184
|
+
function cellsEqual(a, b) {
|
|
18185
|
+
if (a.length !== b.length) return false;
|
|
18186
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
18187
|
+
return true;
|
|
18188
|
+
}
|
|
18189
|
+
function MotionZonesSettings({ deviceId }) {
|
|
18190
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
18191
|
+
const [options, setOptions] = useState(null);
|
|
18192
|
+
const [unsupported, setUnsupported] = useState(false);
|
|
18193
|
+
const [cells, setCells] = useState(null);
|
|
18194
|
+
const [committed, setCommitted] = useState(null);
|
|
18195
|
+
const [saving, setSaving] = useState(false);
|
|
18196
|
+
const [editingGrid, setEditingGrid] = useState(false);
|
|
18197
|
+
const seededRef = useRef(false);
|
|
18198
|
+
useEffect(() => {
|
|
18199
|
+
if (!dev) return void 0;
|
|
18200
|
+
let cancelled = false;
|
|
18201
|
+
(async () => {
|
|
18202
|
+
try {
|
|
18203
|
+
const opts = await dev.motionZones?.getOptions({});
|
|
18204
|
+
if (cancelled || !opts) return;
|
|
18205
|
+
setOptions(opts);
|
|
18206
|
+
if (seededRef.current) return;
|
|
18207
|
+
const status = await dev.motionZones?.getStatus({});
|
|
18208
|
+
if (cancelled || !status) return;
|
|
18209
|
+
seededRef.current = true;
|
|
18210
|
+
const norm = normaliseCells(status.cells, opts);
|
|
18211
|
+
setCommitted(norm);
|
|
18212
|
+
setCells(norm);
|
|
18213
|
+
} catch (err) {
|
|
18214
|
+
if (cancelled) return;
|
|
18215
|
+
if (isAbsentProvider(err)) setUnsupported(true);
|
|
18216
|
+
else console.error("motion-zones load failed", err);
|
|
18217
|
+
}
|
|
18218
|
+
})();
|
|
18219
|
+
return () => {
|
|
18220
|
+
cancelled = true;
|
|
18221
|
+
};
|
|
18222
|
+
}, [dev]);
|
|
18223
|
+
const dirty = useMemo(() => cells !== null && committed !== null && !cellsEqual(cells, committed), [cells, committed]);
|
|
18224
|
+
const activeCount = useMemo(() => cells ? cells.reduce((n, c) => c ? n + 1 : n, 0) : 0, [cells]);
|
|
18225
|
+
const total = options ? options.gridWidth * options.gridHeight : 0;
|
|
18226
|
+
const setAll = useCallback((value) => {
|
|
18227
|
+
if (total > 0) setCells(new Array(total).fill(value));
|
|
18228
|
+
}, [total]);
|
|
18229
|
+
const invert = useCallback(() => {
|
|
18230
|
+
setCells((prev) => prev ? prev.map((c) => !c) : prev);
|
|
18231
|
+
}, []);
|
|
18232
|
+
const revert = useCallback(() => {
|
|
18233
|
+
if (committed) setCells([...committed]);
|
|
18234
|
+
}, [committed]);
|
|
18235
|
+
const save = useCallback(async () => {
|
|
18236
|
+
if (!cells || !dev?.motionZones || !options) return;
|
|
18237
|
+
setSaving(true);
|
|
18238
|
+
try {
|
|
18239
|
+
await dev.motionZones.setZone({ patch: { cells: [...cells] } });
|
|
18240
|
+
const fresh = await dev.motionZones.getStatus({});
|
|
18241
|
+
if (fresh) {
|
|
18242
|
+
const norm = normaliseCells(fresh.cells, options);
|
|
18243
|
+
setCommitted(norm);
|
|
18244
|
+
setCells(norm);
|
|
18245
|
+
}
|
|
18246
|
+
} catch (err) {
|
|
18247
|
+
console.error("motion-zones.setZone failed", err);
|
|
18248
|
+
} finally {
|
|
18249
|
+
setSaving(false);
|
|
18250
|
+
}
|
|
18251
|
+
}, [
|
|
18252
|
+
cells,
|
|
18253
|
+
dev,
|
|
18254
|
+
options
|
|
18255
|
+
]);
|
|
18256
|
+
usePlayerOverlayLayer(useMemo(() => editingGrid && !unsupported && options && cells ? {
|
|
18257
|
+
id: "motion-zones",
|
|
18258
|
+
order: 110,
|
|
18259
|
+
node: /* @__PURE__ */ jsx(MotionGridCanvas, {
|
|
18260
|
+
options,
|
|
18261
|
+
cells,
|
|
18262
|
+
onCellsChange: setCells
|
|
18263
|
+
})
|
|
18264
|
+
} : null, [
|
|
18265
|
+
editingGrid,
|
|
18266
|
+
unsupported,
|
|
18267
|
+
options,
|
|
18268
|
+
cells
|
|
18269
|
+
]));
|
|
18270
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18271
|
+
className: "flex flex-col gap-3",
|
|
18272
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
18273
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
18274
|
+
children: "Motion Zones"
|
|
18275
|
+
}), unsupported ? /* @__PURE__ */ jsx("p", {
|
|
18276
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18277
|
+
children: "This camera doesn't expose an on-board motion-detection grid."
|
|
18278
|
+
}) : !(!unsupported && options !== null && cells !== null) ? /* @__PURE__ */ jsx("p", {
|
|
18279
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18280
|
+
children: "Loading the camera's motion grid…"
|
|
18281
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", {
|
|
18282
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18283
|
+
children: [
|
|
18284
|
+
"Toggle",
|
|
18285
|
+
" ",
|
|
18286
|
+
/* @__PURE__ */ jsx("strong", {
|
|
18287
|
+
className: "text-foreground",
|
|
18288
|
+
children: "Edit grid"
|
|
18289
|
+
}),
|
|
18290
|
+
" to paint the region the camera watches for motion directly on the live frame, then ",
|
|
18291
|
+
/* @__PURE__ */ jsx("strong", {
|
|
18292
|
+
className: "text-foreground",
|
|
18293
|
+
children: "Save"
|
|
18294
|
+
}),
|
|
18295
|
+
" to push the mask to the camera. Detection on/off and sensitivity are set in the",
|
|
18296
|
+
" ",
|
|
18297
|
+
/* @__PURE__ */ jsx("strong", {
|
|
18298
|
+
className: "text-foreground",
|
|
18299
|
+
children: "On-camera motion"
|
|
18300
|
+
}),
|
|
18301
|
+
" section."
|
|
18302
|
+
]
|
|
18303
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
18304
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
18305
|
+
children: [
|
|
18306
|
+
/* @__PURE__ */ jsx("button", {
|
|
18307
|
+
type: "button",
|
|
18308
|
+
onClick: () => setEditingGrid((v) => !v),
|
|
18309
|
+
disabled: saving,
|
|
18310
|
+
"aria-pressed": editingGrid,
|
|
18311
|
+
className: editingGrid ? "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors" : "rounded-md border border-border bg-surface px-2.5 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
18312
|
+
children: editingGrid ? "Done editing" : "Edit grid"
|
|
18313
|
+
}),
|
|
18314
|
+
/* @__PURE__ */ jsx("button", {
|
|
18315
|
+
type: "button",
|
|
18316
|
+
onClick: () => setAll(true),
|
|
18317
|
+
disabled: saving,
|
|
18318
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
18319
|
+
children: "All on"
|
|
18320
|
+
}),
|
|
18321
|
+
/* @__PURE__ */ jsx("button", {
|
|
18322
|
+
type: "button",
|
|
18323
|
+
onClick: () => setAll(false),
|
|
18324
|
+
disabled: saving,
|
|
18325
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
18326
|
+
children: "All off"
|
|
18327
|
+
}),
|
|
18328
|
+
/* @__PURE__ */ jsx("button", {
|
|
18329
|
+
type: "button",
|
|
18330
|
+
onClick: invert,
|
|
18331
|
+
disabled: saving,
|
|
18332
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
18333
|
+
children: "Invert"
|
|
18334
|
+
}),
|
|
18335
|
+
/* @__PURE__ */ jsxs("span", {
|
|
18336
|
+
className: `${TEXT_HINT} ml-1 tabular-nums`,
|
|
18337
|
+
children: [
|
|
18338
|
+
activeCount,
|
|
18339
|
+
" / ",
|
|
18340
|
+
total,
|
|
18341
|
+
" cells · ",
|
|
18342
|
+
options.gridWidth,
|
|
18343
|
+
"×",
|
|
18344
|
+
options.gridHeight
|
|
18345
|
+
]
|
|
18346
|
+
}),
|
|
18347
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
18348
|
+
/* @__PURE__ */ jsx("button", {
|
|
18349
|
+
type: "button",
|
|
18350
|
+
onClick: revert,
|
|
18351
|
+
disabled: saving || !dirty,
|
|
18352
|
+
className: "rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors",
|
|
18353
|
+
children: "Revert"
|
|
18354
|
+
}),
|
|
18355
|
+
/* @__PURE__ */ jsx("button", {
|
|
18356
|
+
type: "button",
|
|
18357
|
+
onClick: () => void save(),
|
|
18358
|
+
disabled: saving || !dirty,
|
|
18359
|
+
className: "rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors",
|
|
18360
|
+
children: saving ? "Saving…" : "Save"
|
|
18361
|
+
})
|
|
18362
|
+
]
|
|
18363
|
+
})] })]
|
|
18364
|
+
});
|
|
18365
|
+
}
|
|
18366
|
+
//#endregion
|
|
18367
|
+
//#region src/widgets/host-widgets.ts
|
|
18368
|
+
/** Host widgets — ui-library React components embeddable as a
|
|
18369
|
+
* `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
|
|
18370
|
+
* Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
|
|
18371
|
+
var HOST_WIDGETS = {
|
|
18372
|
+
"host/motion-zones-grid": MotionZonesSettings,
|
|
18373
|
+
"host/ptz-panel": PtzPanel,
|
|
18374
|
+
"host/ptz-autotrack": AutotrackSection
|
|
18375
|
+
};
|
|
18376
|
+
//#endregion
|
|
18377
|
+
//#region src/composites/widget-slot.tsx
|
|
18378
|
+
/**
|
|
18379
|
+
* <WidgetSlot> — single host-side mount point for any addon-contributed
|
|
18380
|
+
* widget. Consumers reference a widget by its public id
|
|
18381
|
+
* (`<addonId>/<stableId>`), the slot looks the widget's metadata up in
|
|
18382
|
+
* the shared `WidgetRegistry`, resolves the component through the
|
|
18383
|
+
* unified `useRemoteComponent` Module-Federation loader (the SAME path
|
|
18384
|
+
* `ContributionRenderer`'s `kind:'remote'` branch uses), validates host
|
|
18385
|
+
* context against the widget's `requires` metadata, and renders one of:
|
|
18386
|
+
* - skeleton placeholder while the bundle is still loading,
|
|
18387
|
+
* - inline error fallback when the widget id is unknown OR the host
|
|
18388
|
+
* didn't supply a required context (`deviceContext` /
|
|
18389
|
+
* `integrationContext`),
|
|
18390
|
+
* - the resolved component otherwise.
|
|
18391
|
+
*
|
|
18392
|
+
* The slot is intentionally STUPID — no styling beyond the skeleton/
|
|
18393
|
+
* error fallback. Layout (card vs inline, sizing) is the host's
|
|
18394
|
+
* responsibility.
|
|
18395
|
+
*/
|
|
18396
|
+
function WidgetSlot(props) {
|
|
18397
|
+
const { widgetId, deviceId } = props;
|
|
18398
|
+
const HostComponent = HOST_WIDGETS[widgetId];
|
|
18399
|
+
if (HostComponent !== void 0) {
|
|
18400
|
+
if (deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
|
|
18401
|
+
widgetId,
|
|
18402
|
+
reason: "missing-device-context"
|
|
18403
|
+
});
|
|
18404
|
+
return /* @__PURE__ */ jsx(HostComponent, { deviceId });
|
|
18405
|
+
}
|
|
18406
|
+
return /* @__PURE__ */ jsx(RemoteWidgetSlot, { ...props });
|
|
18407
|
+
}
|
|
18408
|
+
/**
|
|
18409
|
+
* Module-Federation remote-widget path — looks the widget's metadata up
|
|
18410
|
+
* in the shared `WidgetRegistry` and resolves it through `useRemoteComponent`.
|
|
18411
|
+
*/
|
|
18412
|
+
function RemoteWidgetSlot(props) {
|
|
18413
|
+
const { widgetId } = props;
|
|
18414
|
+
const metadata = useWidgetMetadata(widgetId);
|
|
18415
|
+
if (metadata === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
|
|
18416
|
+
widgetId,
|
|
18417
|
+
reason: "unknown"
|
|
18418
|
+
});
|
|
18419
|
+
return /* @__PURE__ */ jsx(ResolvedWidgetSlot, {
|
|
18420
|
+
...props,
|
|
18421
|
+
metadata
|
|
18422
|
+
});
|
|
18423
|
+
}
|
|
18424
|
+
/**
|
|
18425
|
+
* Inner host — resolves the widget component through the unified
|
|
18426
|
+
* `useRemoteComponent` MF loader (shared with `ContributionRenderer`'s
|
|
18427
|
+
* `kind:'remote'` branch). Mounted only once a `metadata` descriptor is
|
|
18428
|
+
* known, so the hook always receives a valid `remote` descriptor.
|
|
18429
|
+
*/
|
|
18430
|
+
function ResolvedWidgetSlot(props) {
|
|
18431
|
+
const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows, metadata } = props;
|
|
18432
|
+
const Component = useRemoteComponent(metadata.remote, metadata.bundleUrl);
|
|
18433
|
+
const resolvedInstanceId = useMemo(() => instanceId ?? widgetId, [instanceId, widgetId]);
|
|
18434
|
+
if (Component === null) return /* @__PURE__ */ jsx(WidgetSkeleton, {});
|
|
18435
|
+
if (Component === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
|
|
18436
|
+
widgetId,
|
|
18437
|
+
reason: "missing-export"
|
|
18438
|
+
});
|
|
18439
|
+
if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
|
|
18440
|
+
widgetId,
|
|
18441
|
+
reason: "missing-device-context"
|
|
18442
|
+
});
|
|
18443
|
+
if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ jsx(WidgetMissingError, {
|
|
18444
|
+
widgetId,
|
|
18445
|
+
reason: "missing-integration-context"
|
|
18446
|
+
});
|
|
18447
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
18448
|
+
instanceId: resolvedInstanceId,
|
|
18449
|
+
host,
|
|
18450
|
+
config,
|
|
18451
|
+
deviceId,
|
|
18452
|
+
integrationId,
|
|
18453
|
+
size,
|
|
18454
|
+
columns,
|
|
18455
|
+
rows
|
|
18456
|
+
});
|
|
18457
|
+
}
|
|
18458
|
+
function WidgetSkeleton() {
|
|
18459
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18460
|
+
className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
|
|
18461
|
+
children: [
|
|
18462
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
|
|
18463
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
|
|
18464
|
+
/* @__PURE__ */ jsx("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
|
|
18465
|
+
]
|
|
18466
|
+
});
|
|
18467
|
+
}
|
|
18468
|
+
function WidgetMissingError({ widgetId, reason }) {
|
|
18469
|
+
return /* @__PURE__ */ jsx("div", {
|
|
18470
|
+
className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
|
|
18471
|
+
children: reason === "unknown" ? `Widget "${widgetId}" not registered` : reason === "missing-export" ? `Widget "${widgetId}" bundle does not export this stableId` : reason === "missing-device-context" ? `Widget "${widgetId}" requires a deviceId` : `Widget "${widgetId}" requires an integrationId`
|
|
18472
|
+
});
|
|
18473
|
+
}
|
|
18474
|
+
//#endregion
|
|
18475
|
+
//#region src/composites/config-form-field.tsx
|
|
18476
|
+
var INPUT_CLASS = "w-full rounded-md border border-border bg-background px-2.5 py-1.5 text-xs text-foreground focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none disabled:opacity-50 disabled:cursor-not-allowed";
|
|
18477
|
+
var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
|
|
18478
|
+
var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
|
|
18479
|
+
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
18480
|
+
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
18481
|
+
const resolvedLabel = resolveLabel(label, translationFn);
|
|
18482
|
+
const resolvedDescription = resolveLabel(description, translationFn);
|
|
18483
|
+
const hasLabel = resolvedLabel !== void 0 && resolvedLabel !== "";
|
|
18484
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
18485
|
+
className: `${colSpanClass} min-w-0`,
|
|
18486
|
+
children: [
|
|
18487
|
+
hasLabel && /* @__PURE__ */ jsxs("label", {
|
|
18488
|
+
className: LABEL_CLASS,
|
|
18489
|
+
children: [resolvedLabel, required && /* @__PURE__ */ jsx("span", {
|
|
18490
|
+
className: "text-danger ml-0.5",
|
|
18491
|
+
children: "*"
|
|
18492
|
+
})]
|
|
18493
|
+
}),
|
|
18494
|
+
children,
|
|
18495
|
+
resolvedDescription && /* @__PURE__ */ jsx("p", {
|
|
18496
|
+
className: DESC_CLASS,
|
|
18497
|
+
children: resolvedDescription
|
|
18498
|
+
})
|
|
18499
|
+
]
|
|
18500
|
+
});
|
|
18501
|
+
}
|
|
18502
|
+
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
18503
|
+
return /* @__PURE__ */ jsx(FieldWrapper, {
|
|
18504
|
+
label: field.label,
|
|
18505
|
+
description: field.description,
|
|
18506
|
+
required: field.required,
|
|
18507
|
+
span: field.span,
|
|
18508
|
+
translationFn,
|
|
18509
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
18510
|
+
type: field.inputType ?? "text",
|
|
18511
|
+
className: INPUT_CLASS,
|
|
18512
|
+
value: value === void 0 || value === null ? "" : String(value),
|
|
18513
|
+
placeholder: field.placeholder,
|
|
18514
|
+
maxLength: field.maxLength,
|
|
18515
|
+
pattern: field.pattern,
|
|
18516
|
+
disabled: disabled || field.disabled,
|
|
18517
|
+
onChange: (e) => onChange(e.target.value)
|
|
18518
|
+
})
|
|
18519
|
+
});
|
|
18520
|
+
}
|
|
18521
|
+
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
18522
|
+
const [local, setLocal] = useState(value === void 0 || value === null ? "" : String(value));
|
|
18523
|
+
const focusedRef = useRef(false);
|
|
18524
|
+
useEffect(() => {
|
|
18525
|
+
if (focusedRef.current) return;
|
|
18526
|
+
setLocal(value === void 0 || value === null ? "" : String(value));
|
|
18527
|
+
}, [value]);
|
|
18528
|
+
const handleChange = (raw) => {
|
|
18529
|
+
setLocal(raw);
|
|
18530
|
+
if (raw === "" || raw === "-") {
|
|
18531
|
+
onChange(void 0);
|
|
18532
|
+
return;
|
|
18533
|
+
}
|
|
18534
|
+
const parsed = Number(raw);
|
|
18535
|
+
if (Number.isNaN(parsed)) return;
|
|
18536
|
+
onChange(parsed);
|
|
18537
|
+
};
|
|
18538
|
+
return /* @__PURE__ */ jsx(FieldWrapper, {
|
|
18539
|
+
label: field.label,
|
|
18540
|
+
description: field.description,
|
|
18541
|
+
required: field.required,
|
|
18542
|
+
span: field.span,
|
|
18543
|
+
translationFn,
|
|
18544
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
18545
|
+
className: "flex items-center gap-1",
|
|
18546
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
18547
|
+
type: "number",
|
|
18548
|
+
className: INPUT_CLASS,
|
|
18549
|
+
value: local,
|
|
18550
|
+
placeholder: field.placeholder,
|
|
18551
|
+
min: field.min,
|
|
18552
|
+
max: field.max,
|
|
18553
|
+
step: field.step,
|
|
18554
|
+
disabled: disabled || field.disabled,
|
|
18555
|
+
onFocus: () => {
|
|
18556
|
+
focusedRef.current = true;
|
|
18557
|
+
},
|
|
18558
|
+
onBlur: () => {
|
|
18559
|
+
focusedRef.current = false;
|
|
18560
|
+
},
|
|
18561
|
+
onChange: (e) => handleChange(e.target.value)
|
|
18562
|
+
}), field.unit && /* @__PURE__ */ jsx("span", {
|
|
18563
|
+
className: "text-xs text-foreground-subtle whitespace-nowrap",
|
|
17141
18564
|
children: field.unit
|
|
17142
18565
|
})]
|
|
17143
18566
|
})
|
|
@@ -18246,18 +19669,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
|
|
|
18246
19669
|
}
|
|
18247
19670
|
function WidgetField({ field }) {
|
|
18248
19671
|
const deviceId = useDeviceId();
|
|
18249
|
-
return /* @__PURE__ */ jsx(
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
widgetId: field.widgetId,
|
|
18256
|
-
host: "device-tab",
|
|
18257
|
-
config: field.widgetConfig,
|
|
18258
|
-
deviceId: deviceId ?? void 0,
|
|
18259
|
-
instanceId: field.key
|
|
18260
|
-
})
|
|
19672
|
+
return /* @__PURE__ */ jsx(WidgetSlot, {
|
|
19673
|
+
widgetId: field.widgetId,
|
|
19674
|
+
host: "device-tab",
|
|
19675
|
+
config: field.widgetConfig,
|
|
19676
|
+
deviceId: deviceId ?? void 0,
|
|
19677
|
+
instanceId: field.key
|
|
18261
19678
|
});
|
|
18262
19679
|
}
|
|
18263
19680
|
function formatReadonlyValue(value, unit) {
|
|
@@ -18748,7 +20165,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
|
|
|
18748
20165
|
* + patch loop. `onAfterChange` fires once the patch is applied so
|
|
18749
20166
|
* consumers can re-read any derived state.
|
|
18750
20167
|
*/
|
|
18751
|
-
var Chevron = ({ open }) => /* @__PURE__ */ jsx("svg", {
|
|
20168
|
+
var Chevron$1 = ({ open }) => /* @__PURE__ */ jsx("svg", {
|
|
18752
20169
|
className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
|
|
18753
20170
|
viewBox: "0 0 24 24",
|
|
18754
20171
|
fill: "none",
|
|
@@ -18913,7 +20330,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
|
|
|
18913
20330
|
className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
|
|
18914
20331
|
"aria-expanded": open,
|
|
18915
20332
|
children: [
|
|
18916
|
-
/* @__PURE__ */ jsx(Chevron, { open }),
|
|
20333
|
+
/* @__PURE__ */ jsx(Chevron$1, { open }),
|
|
18917
20334
|
/* @__PURE__ */ jsx("h3", {
|
|
18918
20335
|
className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
|
|
18919
20336
|
children: title ?? addonId
|
|
@@ -19483,184 +20900,38 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
19483
20900
|
}) : /* @__PURE__ */ jsx("svg", {
|
|
19484
20901
|
className: "h-3.5 w-3.5",
|
|
19485
20902
|
viewBox: "0 0 24 24",
|
|
19486
|
-
fill: "none",
|
|
19487
|
-
stroke: "currentColor",
|
|
19488
|
-
strokeWidth: "2",
|
|
19489
|
-
strokeLinecap: "round",
|
|
19490
|
-
children: /* @__PURE__ */ jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
|
|
19491
|
-
})
|
|
19492
|
-
})]
|
|
19493
|
-
})]
|
|
19494
|
-
})
|
|
19495
|
-
]
|
|
19496
|
-
});
|
|
19497
|
-
}
|
|
19498
|
-
function ToolbarButton$1({ onClick, title, children }) {
|
|
19499
|
-
return /* @__PURE__ */ jsx("button", {
|
|
19500
|
-
onClick,
|
|
19501
|
-
title,
|
|
19502
|
-
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
19503
|
-
children
|
|
19504
|
-
});
|
|
19505
|
-
}
|
|
19506
|
-
async function waitForIceGathering(pc) {
|
|
19507
|
-
if (pc.iceGatheringState === "complete") return;
|
|
19508
|
-
return new Promise((resolve) => {
|
|
19509
|
-
const handler = () => {
|
|
19510
|
-
if (pc.iceGatheringState === "complete") {
|
|
19511
|
-
pc.removeEventListener("icegatheringstatechange", handler);
|
|
19512
|
-
resolve();
|
|
19513
|
-
}
|
|
19514
|
-
};
|
|
19515
|
-
pc.addEventListener("icegatheringstatechange", handler);
|
|
19516
|
-
setTimeout(resolve, 5e3);
|
|
19517
|
-
});
|
|
19518
|
-
}
|
|
19519
|
-
//#endregion
|
|
19520
|
-
//#region src/contexts/player-overlays.tsx
|
|
19521
|
-
/**
|
|
19522
|
-
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
19523
|
-
* the device-detail live-frame `StreamPanel`.
|
|
19524
|
-
*
|
|
19525
|
-
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
19526
|
-
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
19527
|
-
* editing state by hand. Adding a new feature (audio waveform,
|
|
19528
|
-
* detection bbox tracker, recording controls, …) meant another
|
|
19529
|
-
* round of plumbing through the host file and StreamPanel's prop
|
|
19530
|
-
* surface. The registry lets a sibling component (Detection tab,
|
|
19531
|
-
* Live Stats tab, an addon-page extension) declare its own layer
|
|
19532
|
-
* imperatively via a hook, and the host renders whatever's
|
|
19533
|
-
* registered.
|
|
19534
|
-
*
|
|
19535
|
-
* Two registries live side-by-side:
|
|
19536
|
-
*
|
|
19537
|
-
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
19538
|
-
* video frame (zones polygon canvas, motion bbox overlay,
|
|
19539
|
-
* audio waveform). Rendered ordered by `order` (lower first
|
|
19540
|
-
* → bottom; higher → top); the last-registered layer wins
|
|
19541
|
-
* ties.
|
|
19542
|
-
*
|
|
19543
|
-
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
19544
|
-
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
19545
|
-
* Each carries an icon, label, controlled `active` flag, and
|
|
19546
|
-
* a click handler. The host (StreamPanel) renders them
|
|
19547
|
-
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
19548
|
-
*
|
|
19549
|
-
* Lifecycle: hooks register their layer / button on mount and
|
|
19550
|
-
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
19551
|
-
* automatically tears down the registration. Re-registering with
|
|
19552
|
-
* the same `id` replaces the prior entry — meaning a single hook
|
|
19553
|
-
* call can update its overlay/button props on every render without
|
|
19554
|
-
* leaking entries.
|
|
19555
|
-
*
|
|
19556
|
-
* Provider scope: typically wraps the whole device-detail subtree
|
|
19557
|
-
* so the StreamPanel + every tab share the same registry. One
|
|
19558
|
-
* provider per `deviceId`; switching device IDs unmounts the
|
|
19559
|
-
* provider naturally.
|
|
19560
|
-
*/
|
|
19561
|
-
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
19562
|
-
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
19563
|
-
function PlayerOverlaysProvider({ children }) {
|
|
19564
|
-
const [layers, setLayers] = useState(() => /* @__PURE__ */ new Map());
|
|
19565
|
-
const [buttons, setButtons] = useState(() => /* @__PURE__ */ new Map());
|
|
19566
|
-
const setLayer = useCallback((layer) => {
|
|
19567
|
-
setLayers((prev) => {
|
|
19568
|
-
const next = new Map(prev);
|
|
19569
|
-
next.set(layer.id, layer);
|
|
19570
|
-
return next;
|
|
19571
|
-
});
|
|
19572
|
-
}, []);
|
|
19573
|
-
const removeLayer = useCallback((id) => {
|
|
19574
|
-
setLayers((prev) => {
|
|
19575
|
-
if (!prev.has(id)) return prev;
|
|
19576
|
-
const next = new Map(prev);
|
|
19577
|
-
next.delete(id);
|
|
19578
|
-
return next;
|
|
19579
|
-
});
|
|
19580
|
-
}, []);
|
|
19581
|
-
const setButton = useCallback((button) => {
|
|
19582
|
-
setButtons((prev) => {
|
|
19583
|
-
const next = new Map(prev);
|
|
19584
|
-
next.set(button.id, button);
|
|
19585
|
-
return next;
|
|
19586
|
-
});
|
|
19587
|
-
}, []);
|
|
19588
|
-
const removeButton = useCallback((id) => {
|
|
19589
|
-
setButtons((prev) => {
|
|
19590
|
-
if (!prev.has(id)) return prev;
|
|
19591
|
-
const next = new Map(prev);
|
|
19592
|
-
next.delete(id);
|
|
19593
|
-
return next;
|
|
19594
|
-
});
|
|
19595
|
-
}, []);
|
|
19596
|
-
const stateValue = useMemo(() => ({
|
|
19597
|
-
layers,
|
|
19598
|
-
buttons
|
|
19599
|
-
}), [layers, buttons]);
|
|
19600
|
-
const actionsValue = useMemo(() => ({
|
|
19601
|
-
setLayer,
|
|
19602
|
-
removeLayer,
|
|
19603
|
-
setButton,
|
|
19604
|
-
removeButton
|
|
19605
|
-
}), [
|
|
19606
|
-
setLayer,
|
|
19607
|
-
removeLayer,
|
|
19608
|
-
setButton,
|
|
19609
|
-
removeButton
|
|
19610
|
-
]);
|
|
19611
|
-
return /* @__PURE__ */ jsx(PlayerOverlaysStateContext.Provider, {
|
|
19612
|
-
value: stateValue,
|
|
19613
|
-
children: /* @__PURE__ */ jsx(PlayerOverlaysActionsContext.Provider, {
|
|
19614
|
-
value: actionsValue,
|
|
19615
|
-
children
|
|
19616
|
-
})
|
|
20903
|
+
fill: "none",
|
|
20904
|
+
stroke: "currentColor",
|
|
20905
|
+
strokeWidth: "2",
|
|
20906
|
+
strokeLinecap: "round",
|
|
20907
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" })
|
|
20908
|
+
})
|
|
20909
|
+
})]
|
|
20910
|
+
})]
|
|
20911
|
+
})
|
|
20912
|
+
]
|
|
19617
20913
|
});
|
|
19618
20914
|
}
|
|
19619
|
-
|
|
19620
|
-
|
|
19621
|
-
|
|
19622
|
-
|
|
19623
|
-
|
|
19624
|
-
|
|
19625
|
-
|
|
19626
|
-
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
19627
|
-
}, [state]);
|
|
19628
|
-
}
|
|
19629
|
-
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
19630
|
-
function usePlayerToolbarButtons() {
|
|
19631
|
-
const state = useContext(PlayerOverlaysStateContext);
|
|
19632
|
-
return useMemo(() => {
|
|
19633
|
-
if (!state) return [];
|
|
19634
|
-
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
19635
|
-
}, [state]);
|
|
19636
|
-
}
|
|
19637
|
-
/**
|
|
19638
|
-
* Register an overlay layer for the lifetime of the calling component.
|
|
19639
|
-
* Re-registers on every render with the latest spec; auto-unregisters
|
|
19640
|
-
* on unmount. Pass `null` to skip registration when the layer is
|
|
19641
|
-
* conditionally enabled (the hook still runs every render — keeps
|
|
19642
|
-
* react-hook order stable across spec === null toggles).
|
|
19643
|
-
*
|
|
19644
|
-
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
19645
|
-
* to avoid re-registering on every parent render — context-write
|
|
19646
|
-
* effects depend on referential equality of the spec object.
|
|
19647
|
-
*/
|
|
19648
|
-
function usePlayerOverlayLayer(spec) {
|
|
19649
|
-
const actions = useContext(PlayerOverlaysActionsContext);
|
|
19650
|
-
useEffect(() => {
|
|
19651
|
-
if (!actions || !spec) return void 0;
|
|
19652
|
-
actions.setLayer(spec);
|
|
19653
|
-
return () => actions.removeLayer(spec.id);
|
|
19654
|
-
}, [actions, spec]);
|
|
20915
|
+
function ToolbarButton$1({ onClick, title, children }) {
|
|
20916
|
+
return /* @__PURE__ */ jsx("button", {
|
|
20917
|
+
onClick,
|
|
20918
|
+
title,
|
|
20919
|
+
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
20920
|
+
children
|
|
20921
|
+
});
|
|
19655
20922
|
}
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19660
|
-
|
|
19661
|
-
|
|
19662
|
-
|
|
19663
|
-
|
|
20923
|
+
async function waitForIceGathering(pc) {
|
|
20924
|
+
if (pc.iceGatheringState === "complete") return;
|
|
20925
|
+
return new Promise((resolve) => {
|
|
20926
|
+
const handler = () => {
|
|
20927
|
+
if (pc.iceGatheringState === "complete") {
|
|
20928
|
+
pc.removeEventListener("icegatheringstatechange", handler);
|
|
20929
|
+
resolve();
|
|
20930
|
+
}
|
|
20931
|
+
};
|
|
20932
|
+
pc.addEventListener("icegatheringstatechange", handler);
|
|
20933
|
+
setTimeout(resolve, 5e3);
|
|
20934
|
+
});
|
|
19664
20935
|
}
|
|
19665
20936
|
//#endregion
|
|
19666
20937
|
//#region src/composites/stream-panel.tsx
|
|
@@ -22146,7 +23417,7 @@ var LEVEL_OPTIONS = [
|
|
|
22146
23417
|
"error"
|
|
22147
23418
|
];
|
|
22148
23419
|
/** Max live entries kept in the ring buffer. */
|
|
22149
|
-
var MAX_LIVE_ENTRIES$
|
|
23420
|
+
var MAX_LIVE_ENTRIES$1 = 500;
|
|
22150
23421
|
function passesLevel(logLevel, filterLevel) {
|
|
22151
23422
|
if (!filterLevel) return true;
|
|
22152
23423
|
return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
|
|
@@ -22175,7 +23446,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22175
23446
|
integrationId,
|
|
22176
23447
|
requestId
|
|
22177
23448
|
]);
|
|
22178
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$
|
|
23449
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
|
|
22179
23450
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22180
23451
|
const liveLogs = buffer.entries;
|
|
22181
23452
|
const [clearedAt, setClearedAt] = useState(0);
|
|
@@ -22502,7 +23773,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22502
23773
|
}),
|
|
22503
23774
|
/* @__PURE__ */ jsx("td", {
|
|
22504
23775
|
className: "px-1 py-1 align-top w-5",
|
|
22505
|
-
children: /* @__PURE__ */ jsx(RowCopyButton$
|
|
23776
|
+
children: /* @__PURE__ */ jsx(RowCopyButton$1, {
|
|
22506
23777
|
copied: copiedRowKey === key,
|
|
22507
23778
|
onCopy: () => copyOne(log, key)
|
|
22508
23779
|
})
|
|
@@ -22541,7 +23812,7 @@ function ScopeBadge$2({ label, value }) {
|
|
|
22541
23812
|
* "did it work" affordance — same UX language the toolbar Copy
|
|
22542
23813
|
* button already uses for its `Copied!` label.
|
|
22543
23814
|
*/
|
|
22544
|
-
function RowCopyButton$
|
|
23815
|
+
function RowCopyButton$1({ copied, onCopy }) {
|
|
22545
23816
|
return /* @__PURE__ */ jsx("button", {
|
|
22546
23817
|
type: "button",
|
|
22547
23818
|
title: "Copy this row",
|
|
@@ -22866,7 +24137,7 @@ function hasContent(entry) {
|
|
|
22866
24137
|
if (cat === EventCategory.DetectionResult || cat === EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
|
|
22867
24138
|
return true;
|
|
22868
24139
|
}
|
|
22869
|
-
var MAX_LIVE_ENTRIES
|
|
24140
|
+
var MAX_LIVE_ENTRIES = 300;
|
|
22870
24141
|
function EventStream({ agentId, addonId, deviceId, defaultCategories, categories: legacyCategories, category: propsCategory, maxHeight = "max-h-96", limit = 50, showCategoryFilter = true, showFilters: _showFilters = false, liveBuffer: externalBuffer, onClose, className }) {
|
|
22871
24142
|
const defaultsArray = useMemo(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
|
|
22872
24143
|
const defaultsKey = useMemo(() => [...defaultsArray].sort().join(","), [defaultsArray]);
|
|
@@ -22878,7 +24149,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
22878
24149
|
setActiveCategories(new Set(defaultsArray));
|
|
22879
24150
|
}
|
|
22880
24151
|
}, [defaultsKey, defaultsArray]);
|
|
22881
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES
|
|
24152
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
|
|
22882
24153
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22883
24154
|
const liveEvents = buffer.entries;
|
|
22884
24155
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
@@ -23202,7 +24473,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
23202
24473
|
className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
|
|
23203
24474
|
children: new Date(event.timestamp).toLocaleTimeString()
|
|
23204
24475
|
}),
|
|
23205
|
-
/* @__PURE__ */ jsx(RowCopyButton
|
|
24476
|
+
/* @__PURE__ */ jsx(RowCopyButton, {
|
|
23206
24477
|
copied: copiedRowId === event.id,
|
|
23207
24478
|
onCopy: () => copyOne(event)
|
|
23208
24479
|
}),
|
|
@@ -23392,7 +24663,7 @@ function ScopeBadge$1({ label, value }) {
|
|
|
23392
24663
|
* click doesn't toggle the row's expand state. Flashes a Check
|
|
23393
24664
|
* for ~1s after copy.
|
|
23394
24665
|
*/
|
|
23395
|
-
function RowCopyButton
|
|
24666
|
+
function RowCopyButton({ copied, onCopy }) {
|
|
23396
24667
|
return /* @__PURE__ */ jsx("button", {
|
|
23397
24668
|
type: "button",
|
|
23398
24669
|
title: "Copy this event",
|
|
@@ -23659,162 +24930,137 @@ function StatusBadge$1({ status }) {
|
|
|
23659
24930
|
//#endregion
|
|
23660
24931
|
//#region src/composites/state-values-stream.tsx
|
|
23661
24932
|
/**
|
|
23662
|
-
* StateValuesStream —
|
|
24933
|
+
* StateValuesStream — live current-state tree for ui-library.
|
|
24934
|
+
*
|
|
24935
|
+
* Renders a single device's CURRENT runtime-state as a scrollable,
|
|
24936
|
+
* collapsible hierarchical tree (NOT a chronological change log).
|
|
23663
24937
|
*
|
|
23664
|
-
*
|
|
23665
|
-
*
|
|
23666
|
-
*
|
|
24938
|
+
* - Seed: on mount, `trpc.deviceState.getAllSnapshots` returns the
|
|
24939
|
+
* whole-system snapshot; we pick out this device's entry and build
|
|
24940
|
+
* a `Map<capName, slice>` of its current state.
|
|
24941
|
+
* - Realtime: an `EventCategory.DeviceStateChanged` tRPC subscription
|
|
24942
|
+
* scoped to the device patches each cap's slice into the map in
|
|
24943
|
+
* place — the matching tree node re-renders, no rows are appended.
|
|
24944
|
+
* - Tree: top level = cap names (sorted, expanded by default); each
|
|
24945
|
+
* cap node expands to its slice keys; nested objects/arrays expand
|
|
24946
|
+
* recursively, deep nesting collapsed by default. A per-cap
|
|
24947
|
+
* "updated Ns ago" indicator surfaces realtime activity.
|
|
24948
|
+
* - Filter/search: cap-name multiselect popover + free-text search,
|
|
24949
|
+
* both applied to the tree (filter which caps show; search matches
|
|
24950
|
+
* keys/values anywhere in the slice).
|
|
23667
24951
|
*
|
|
23668
|
-
*
|
|
24952
|
+
* The live subscription is mounted only while this component renders,
|
|
23669
24953
|
* so the parent can mount/unmount it on tab switches and the tRPC
|
|
23670
24954
|
* subscription is torn down automatically when hidden.
|
|
23671
24955
|
*/
|
|
23672
|
-
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
if (e.category !== EventCategory.DeviceStateChanged) return false;
|
|
23676
|
-
const data = e.data;
|
|
23677
|
-
if (!data || typeof data !== "object") return false;
|
|
23678
|
-
if (data.deviceId !== deviceId) return false;
|
|
23679
|
-
if (typeof data.capName !== "string") return false;
|
|
23680
|
-
if (!data.slice || typeof data.slice !== "object") return false;
|
|
23681
|
-
return true;
|
|
23682
|
-
}
|
|
23683
|
-
function toEntry(raw) {
|
|
24956
|
+
/** Narrow an untrusted live-stream event to a `DeviceStateChanged`
|
|
24957
|
+
* for THIS device, returning the cap name + slice it carries. */
|
|
24958
|
+
function readStateChange(raw, deviceId) {
|
|
23684
24959
|
if (!raw || typeof raw !== "object") return null;
|
|
23685
24960
|
const e = raw;
|
|
23686
|
-
|
|
23687
|
-
const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
|
|
24961
|
+
if (e.category !== EventCategory.DeviceStateChanged) return null;
|
|
23688
24962
|
const data = e.data;
|
|
23689
|
-
if (!
|
|
23690
|
-
if (
|
|
24963
|
+
if (!data || typeof data !== "object") return null;
|
|
24964
|
+
if (data.deviceId !== deviceId) return null;
|
|
24965
|
+
if (typeof data.capName !== "string") return null;
|
|
24966
|
+
if (!data.slice || typeof data.slice !== "object") return null;
|
|
23691
24967
|
return {
|
|
23692
|
-
id,
|
|
23693
|
-
timestamp,
|
|
23694
24968
|
capName: data.capName,
|
|
23695
24969
|
slice: data.slice
|
|
23696
24970
|
};
|
|
23697
24971
|
}
|
|
23698
|
-
|
|
24972
|
+
function isSystemSnapshot(raw) {
|
|
24973
|
+
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
24974
|
+
}
|
|
23699
24975
|
/**
|
|
23700
24976
|
* Every device-scoped cap name, sorted alphabetically — same shape
|
|
23701
24977
|
* as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
|
|
23702
24978
|
* surface is symmetric across the three live-streams (Logs, Events,
|
|
23703
|
-
* State). Computed once at module load
|
|
23704
|
-
* full list rather than only the caps that have already emitted at
|
|
23705
|
-
* least one slice change in the current session.
|
|
24979
|
+
* State). Computed once at module load.
|
|
23706
24980
|
*/
|
|
23707
24981
|
var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
|
|
23708
|
-
|
|
24982
|
+
/** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
|
|
24983
|
+
* their direct slice keys (depth 1) render expanded; anything nested
|
|
24984
|
+
* deeper opens on demand. */
|
|
24985
|
+
var DEFAULT_EXPAND_DEPTH = 1;
|
|
24986
|
+
function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
|
|
23709
24987
|
const [activeCaps, setActiveCaps] = useState(useMemo(() => new Set(defaultCaps ?? []), [defaultCaps]));
|
|
23710
24988
|
const [searchText, setSearchText] = useState("");
|
|
23711
|
-
const [autoScroll, setAutoScroll] = useState(true);
|
|
23712
|
-
const [expandedRows, setExpandedRows] = useState(/* @__PURE__ */ new Set());
|
|
23713
|
-
const [clearedAt, setClearedAt] = useState(0);
|
|
23714
24989
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
23715
24990
|
const [filterSearch, setFilterSearch] = useState("");
|
|
23716
24991
|
const filterRootRef = useRef(null);
|
|
23717
|
-
const
|
|
23718
|
-
const
|
|
23719
|
-
const
|
|
23720
|
-
const scrollRef = useRef(null);
|
|
24992
|
+
const [capStates, setCapStates] = useState(/* @__PURE__ */ new Map());
|
|
24993
|
+
const [collapseOverrides, setCollapseOverrides] = useState(/* @__PURE__ */ new Map());
|
|
24994
|
+
const [, setNowTick] = useState(0);
|
|
23721
24995
|
const prevDeviceRef = useRef(deviceId);
|
|
23722
24996
|
useEffect(() => {
|
|
23723
24997
|
if (prevDeviceRef.current !== deviceId) {
|
|
23724
24998
|
prevDeviceRef.current = deviceId;
|
|
23725
|
-
|
|
23726
|
-
|
|
23727
|
-
setClearedAt(0);
|
|
24999
|
+
setCapStates(/* @__PURE__ */ new Map());
|
|
25000
|
+
setCollapseOverrides(/* @__PURE__ */ new Map());
|
|
23728
25001
|
}
|
|
23729
|
-
}, [deviceId
|
|
23730
|
-
const
|
|
23731
|
-
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
|
|
23736
|
-
|
|
25002
|
+
}, [deviceId]);
|
|
25003
|
+
const { data: snapshot, isLoading } = trpc.deviceState.getAllSnapshots.useQuery({}, { staleTime: 3e4 });
|
|
25004
|
+
const seededRef = useRef(false);
|
|
25005
|
+
useEffect(() => {
|
|
25006
|
+
if (seededRef.current) return;
|
|
25007
|
+
if (!isSystemSnapshot(snapshot)) return;
|
|
25008
|
+
const deviceSlices = snapshot[String(deviceId)];
|
|
25009
|
+
seededRef.current = true;
|
|
25010
|
+
if (!deviceSlices) return;
|
|
25011
|
+
const now = Date.now();
|
|
25012
|
+
setCapStates((prev) => {
|
|
25013
|
+
const next = new Map(prev);
|
|
25014
|
+
for (const [capName, slice] of Object.entries(deviceSlices)) {
|
|
25015
|
+
if (next.has(capName)) continue;
|
|
25016
|
+
next.set(capName, {
|
|
25017
|
+
slice,
|
|
25018
|
+
updatedAt: now
|
|
25019
|
+
});
|
|
25020
|
+
}
|
|
25021
|
+
return next;
|
|
25022
|
+
});
|
|
25023
|
+
}, [snapshot, deviceId]);
|
|
23737
25024
|
useEffect(() => {
|
|
23738
|
-
|
|
23739
|
-
}, [
|
|
25025
|
+
seededRef.current = false;
|
|
25026
|
+
}, [deviceId]);
|
|
23740
25027
|
trpc.systemEvents.subscribe.useSubscription({
|
|
23741
25028
|
deviceId,
|
|
23742
25029
|
category: EventCategory.DeviceStateChanged
|
|
23743
25030
|
}, { onData: (raw) => {
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
23748
|
-
|
|
23749
|
-
|
|
25031
|
+
const change = readStateChange(raw, deviceId);
|
|
25032
|
+
if (!change) return;
|
|
25033
|
+
setCapStates((prev) => {
|
|
25034
|
+
const next = new Map(prev);
|
|
25035
|
+
next.set(change.capName, {
|
|
25036
|
+
slice: change.slice,
|
|
25037
|
+
updatedAt: Date.now()
|
|
25038
|
+
});
|
|
25039
|
+
return next;
|
|
25040
|
+
});
|
|
23750
25041
|
} });
|
|
23751
|
-
const prevLiveCountRef = useRef(liveEvents.length);
|
|
23752
25042
|
useEffect(() => {
|
|
23753
|
-
const
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
const
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
else if (!autoScroll && el.scrollTop > 0) {
|
|
23763
|
-
const firstRow = el.firstElementChild?.firstElementChild;
|
|
23764
|
-
const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
|
|
23765
|
-
const newCount = liveEvents.length - prevCount;
|
|
23766
|
-
el.scrollTop += rowHeight * newCount;
|
|
23767
|
-
}
|
|
23768
|
-
}, [liveEvents.length, autoScroll]);
|
|
23769
|
-
const allEntries = useMemo(() => {
|
|
23770
|
-
const byId = /* @__PURE__ */ new Map();
|
|
23771
|
-
for (const e of initialEvents ?? []) {
|
|
23772
|
-
const entry = toEntry(e);
|
|
23773
|
-
if (entry) byId.set(entry.id, entry);
|
|
23774
|
-
}
|
|
23775
|
-
for (const e of liveEvents) byId.set(e.id, e);
|
|
23776
|
-
let list = [...byId.values()];
|
|
23777
|
-
if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
|
|
23778
|
-
if (activeCaps.size > 0) list = list.filter((e) => activeCaps.has(e.capName));
|
|
25043
|
+
const id = setInterval(() => setNowTick((t) => t + 1), 5e3);
|
|
25044
|
+
return () => clearInterval(id);
|
|
25045
|
+
}, []);
|
|
25046
|
+
const visibleCaps = useMemo(() => {
|
|
25047
|
+
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
25048
|
+
for (const capName of capStates.keys()) caps.add(capName);
|
|
25049
|
+
return [...caps].sort();
|
|
25050
|
+
}, [capStates]);
|
|
25051
|
+
const filteredCapNames = useMemo(() => {
|
|
23779
25052
|
const needle = searchText.trim().toLowerCase();
|
|
23780
|
-
|
|
23781
|
-
if (
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
try {
|
|
23786
|
-
if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
|
|
23787
|
-
} catch {}
|
|
23788
|
-
try {
|
|
23789
|
-
if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
|
|
23790
|
-
} catch {}
|
|
23791
|
-
return false;
|
|
25053
|
+
return [...capStates.keys()].sort().filter((capName) => {
|
|
25054
|
+
if (activeCaps.size > 0 && !activeCaps.has(capName)) return false;
|
|
25055
|
+
if (!needle) return true;
|
|
25056
|
+
if (capName.toLowerCase().includes(needle)) return true;
|
|
25057
|
+
return sliceMatchesSearch(capStates.get(capName)?.slice ?? {}, needle);
|
|
23792
25058
|
});
|
|
23793
|
-
return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
23794
25059
|
}, [
|
|
23795
|
-
|
|
23796
|
-
liveEvents,
|
|
25060
|
+
capStates,
|
|
23797
25061
|
activeCaps,
|
|
23798
|
-
searchText
|
|
23799
|
-
clearedAt
|
|
25062
|
+
searchText
|
|
23800
25063
|
]);
|
|
23801
|
-
const visibleCaps = useMemo(() => {
|
|
23802
|
-
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
23803
|
-
for (const e of initialEvents ?? []) {
|
|
23804
|
-
const entry = toEntry(e);
|
|
23805
|
-
if (entry) caps.add(entry.capName);
|
|
23806
|
-
}
|
|
23807
|
-
for (const e of liveEvents) caps.add(e.capName);
|
|
23808
|
-
return [...caps].sort();
|
|
23809
|
-
}, [initialEvents, liveEvents]);
|
|
23810
|
-
const toggleRow = useCallback((id) => {
|
|
23811
|
-
setExpandedRows((prev) => {
|
|
23812
|
-
const next = new Set(prev);
|
|
23813
|
-
if (next.has(id)) next.delete(id);
|
|
23814
|
-
else next.add(id);
|
|
23815
|
-
return next;
|
|
23816
|
-
});
|
|
23817
|
-
}, []);
|
|
23818
25064
|
const toggleCap = useCallback((cap) => {
|
|
23819
25065
|
setActiveCaps((prev) => {
|
|
23820
25066
|
const next = new Set(prev);
|
|
@@ -23827,34 +25073,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23827
25073
|
setActiveCaps(new Set(defaultCaps ?? []));
|
|
23828
25074
|
setSearchText("");
|
|
23829
25075
|
}, [defaultCaps]);
|
|
23830
|
-
const handleClear = useCallback(() => {
|
|
23831
|
-
setClearedAt(Date.now());
|
|
23832
|
-
buffer.reset();
|
|
23833
|
-
}, [buffer]);
|
|
23834
|
-
const [copiedAll, setCopiedAll] = useState(false);
|
|
23835
|
-
const [copiedRowId, setCopiedRowId] = useState(null);
|
|
23836
|
-
const formatEntryForCopy = useCallback((entry) => {
|
|
23837
|
-
return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
|
|
23838
|
-
}, []);
|
|
23839
|
-
const handleCopyAll = useCallback(() => {
|
|
23840
|
-
const lines = allEntries.map(formatEntryForCopy);
|
|
23841
|
-
navigator.clipboard.writeText(lines.join("\n")).then(() => {
|
|
23842
|
-
setCopiedAll(true);
|
|
23843
|
-
setTimeout(() => setCopiedAll(false), 1500);
|
|
23844
|
-
});
|
|
23845
|
-
}, [allEntries, formatEntryForCopy]);
|
|
23846
|
-
const copyOne = useCallback((entry) => {
|
|
23847
|
-
navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
|
|
23848
|
-
setCopiedRowId(entry.id);
|
|
23849
|
-
setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
|
|
23850
|
-
});
|
|
23851
|
-
}, [formatEntryForCopy]);
|
|
23852
25076
|
const handleSelectAllCaps = useCallback(() => {
|
|
23853
25077
|
setActiveCaps(new Set(visibleCaps));
|
|
23854
25078
|
}, [visibleCaps]);
|
|
23855
25079
|
const handleSelectNoCaps = useCallback(() => {
|
|
23856
25080
|
setActiveCaps(/* @__PURE__ */ new Set());
|
|
23857
25081
|
}, []);
|
|
25082
|
+
const toggleNode = useCallback((path, currentlyOpen) => {
|
|
25083
|
+
setCollapseOverrides((prev) => {
|
|
25084
|
+
const next = new Map(prev);
|
|
25085
|
+
next.set(path, !currentlyOpen);
|
|
25086
|
+
return next;
|
|
25087
|
+
});
|
|
25088
|
+
}, []);
|
|
25089
|
+
const isPathOpen = useCallback((path, depth) => {
|
|
25090
|
+
const override = collapseOverrides.get(path);
|
|
25091
|
+
if (override !== void 0) return override;
|
|
25092
|
+
return depth < DEFAULT_EXPAND_DEPTH;
|
|
25093
|
+
}, [collapseOverrides]);
|
|
25094
|
+
const [copiedAll, setCopiedAll] = useState(false);
|
|
25095
|
+
const handleCopyAll = useCallback(() => {
|
|
25096
|
+
const out = {};
|
|
25097
|
+
for (const capName of filteredCapNames) {
|
|
25098
|
+
const state = capStates.get(capName);
|
|
25099
|
+
if (state) out[capName] = state.slice;
|
|
25100
|
+
}
|
|
25101
|
+
navigator.clipboard.writeText(JSON.stringify(out, null, 2)).then(() => {
|
|
25102
|
+
setCopiedAll(true);
|
|
25103
|
+
setTimeout(() => setCopiedAll(false), 1500);
|
|
25104
|
+
});
|
|
25105
|
+
}, [filteredCapNames, capStates]);
|
|
23858
25106
|
const filteredPopoverCaps = useMemo(() => {
|
|
23859
25107
|
const needle = filterSearch.trim().toLowerCase();
|
|
23860
25108
|
if (!needle) return visibleCaps;
|
|
@@ -23873,6 +25121,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23873
25121
|
document.addEventListener("mousedown", onDocClick);
|
|
23874
25122
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
23875
25123
|
}, [filterOpen]);
|
|
25124
|
+
const searchNeedle = searchText.trim().toLowerCase();
|
|
23876
25125
|
return /* @__PURE__ */ jsxs("div", {
|
|
23877
25126
|
className: cn("h-full min-h-0 flex flex-col", className),
|
|
23878
25127
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -23889,18 +25138,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23889
25138
|
label: "device",
|
|
23890
25139
|
value: `#${deviceId}`
|
|
23891
25140
|
}),
|
|
23892
|
-
/* @__PURE__ */ jsxs("button", {
|
|
23893
|
-
type: "button",
|
|
23894
|
-
onClick: () => {
|
|
23895
|
-
setAutoScroll((a) => !a);
|
|
23896
|
-
if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
|
|
23897
|
-
top: 0,
|
|
23898
|
-
behavior: "smooth"
|
|
23899
|
-
});
|
|
23900
|
-
},
|
|
23901
|
-
className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", autoScroll ? "border-border text-foreground-subtle hover:text-foreground" : "border-amber-500/30 bg-amber-500/10 text-amber-400"),
|
|
23902
|
-
children: [/* @__PURE__ */ jsx(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
|
|
23903
|
-
}),
|
|
23904
25141
|
/* @__PURE__ */ jsxs("div", {
|
|
23905
25142
|
className: "relative",
|
|
23906
25143
|
ref: filterRootRef,
|
|
@@ -23908,7 +25145,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23908
25145
|
type: "button",
|
|
23909
25146
|
onClick: () => setFilterOpen((v) => !v),
|
|
23910
25147
|
className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", !allSelected ? "border-violet-500/30 bg-violet-500/10 text-violet-400" : "border-border text-foreground-subtle hover:text-foreground"),
|
|
23911
|
-
title: "Pick caps to
|
|
25148
|
+
title: "Pick caps to show",
|
|
23912
25149
|
children: [
|
|
23913
25150
|
/* @__PURE__ */ jsx(Funnel, { className: "h-2.5 w-2.5" }),
|
|
23914
25151
|
"Filter (",
|
|
@@ -23933,19 +25170,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23933
25170
|
title: "Reset filters",
|
|
23934
25171
|
children: [/* @__PURE__ */ jsx(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
|
|
23935
25172
|
}),
|
|
23936
|
-
/* @__PURE__ */ jsxs("button", {
|
|
23937
|
-
type: "button",
|
|
23938
|
-
onClick: handleClear,
|
|
23939
|
-
className: "inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border border-border text-foreground-subtle hover:text-foreground transition-colors",
|
|
23940
|
-
title: "Clear visible entries",
|
|
23941
|
-
children: [/* @__PURE__ */ jsx(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
|
|
23942
|
-
}),
|
|
23943
25173
|
/* @__PURE__ */ jsxs("button", {
|
|
23944
25174
|
type: "button",
|
|
23945
25175
|
onClick: handleCopyAll,
|
|
23946
|
-
disabled:
|
|
23947
|
-
className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground",
|
|
23948
|
-
title: "Copy
|
|
25176
|
+
disabled: filteredCapNames.length === 0,
|
|
25177
|
+
className: cn("inline-flex items-center gap-1 text-[9px] px-1.5 py-0.5 rounded border transition-colors", copiedAll ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-400" : "border-border text-foreground-subtle hover:text-foreground", filteredCapNames.length === 0 && "opacity-40 cursor-not-allowed"),
|
|
25178
|
+
title: "Copy visible state as JSON",
|
|
23949
25179
|
children: [/* @__PURE__ */ jsx(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
|
|
23950
25180
|
}),
|
|
23951
25181
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -23964,13 +25194,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23964
25194
|
children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
|
|
23965
25195
|
})]
|
|
23966
25196
|
}),
|
|
23967
|
-
|
|
23968
|
-
className: "text-[9px] text-emerald-400 font-medium",
|
|
23969
|
-
children: [
|
|
23970
|
-
"+",
|
|
23971
|
-
liveEvents.length,
|
|
23972
|
-
" live"
|
|
23973
|
-
]
|
|
25197
|
+
/* @__PURE__ */ jsxs("span", {
|
|
25198
|
+
className: "text-[9px] text-emerald-400 font-medium inline-flex items-center gap-1",
|
|
25199
|
+
children: [/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" }), "live"]
|
|
23974
25200
|
})
|
|
23975
25201
|
]
|
|
23976
25202
|
}), onClose && /* @__PURE__ */ jsx("button", {
|
|
@@ -23980,90 +25206,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23980
25206
|
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
23981
25207
|
})]
|
|
23982
25208
|
}), /* @__PURE__ */ jsxs("div", {
|
|
23983
|
-
|
|
23984
|
-
className: cn("flex-1 min-h-0 overflow-auto text-[10px]", maxHeight),
|
|
25209
|
+
className: cn("flex-1 min-h-0 overflow-auto text-[10px] font-mono", maxHeight),
|
|
23985
25210
|
children: [
|
|
23986
|
-
isLoading && /* @__PURE__ */ jsxs("div", {
|
|
23987
|
-
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
|
|
23988
|
-
children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading state
|
|
25211
|
+
isLoading && capStates.size === 0 && /* @__PURE__ */ jsxs("div", {
|
|
25212
|
+
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle font-sans",
|
|
25213
|
+
children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading device state..."]
|
|
23989
25214
|
}),
|
|
23990
|
-
!isLoading &&
|
|
23991
|
-
className: "py-4 text-center text-foreground-subtle",
|
|
23992
|
-
children: "No state
|
|
25215
|
+
!isLoading && capStates.size === 0 && /* @__PURE__ */ jsx("div", {
|
|
25216
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25217
|
+
children: "No runtime state"
|
|
23993
25218
|
}),
|
|
23994
|
-
|
|
23995
|
-
className: "
|
|
23996
|
-
children:
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24011
|
-
}),
|
|
24012
|
-
/* @__PURE__ */ jsxs("div", {
|
|
24013
|
-
className: "flex-1 min-w-0",
|
|
24014
|
-
children: [
|
|
24015
|
-
/* @__PURE__ */ jsxs("div", {
|
|
24016
|
-
className: "flex items-center gap-1.5",
|
|
24017
|
-
children: [
|
|
24018
|
-
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" }),
|
|
24019
|
-
/* @__PURE__ */ jsx("span", {
|
|
24020
|
-
className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium",
|
|
24021
|
-
children: entry.capName
|
|
24022
|
-
}),
|
|
24023
|
-
fields.length > 3 && /* @__PURE__ */ jsxs("span", {
|
|
24024
|
-
className: "text-[9px] text-foreground-subtle",
|
|
24025
|
-
children: [
|
|
24026
|
-
"+",
|
|
24027
|
-
fields.length - 3,
|
|
24028
|
-
" more"
|
|
24029
|
-
]
|
|
24030
|
-
})
|
|
24031
|
-
]
|
|
24032
|
-
}),
|
|
24033
|
-
summary && /* @__PURE__ */ jsx("p", {
|
|
24034
|
-
className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
|
|
24035
|
-
children: summary
|
|
24036
|
-
}),
|
|
24037
|
-
expanded && /* @__PURE__ */ jsx("div", {
|
|
24038
|
-
className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
|
|
24039
|
-
children: fields.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
24040
|
-
className: "text-foreground-subtle italic",
|
|
24041
|
-
children: "empty slice"
|
|
24042
|
-
}) : fields.map(([k, v]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
24043
|
-
/* @__PURE__ */ jsx("span", {
|
|
24044
|
-
className: "text-primary/70",
|
|
24045
|
-
children: k
|
|
24046
|
-
}),
|
|
24047
|
-
/* @__PURE__ */ jsx("span", {
|
|
24048
|
-
className: "text-foreground-subtle",
|
|
24049
|
-
children: ": "
|
|
24050
|
-
}),
|
|
24051
|
-
/* @__PURE__ */ jsx("span", {
|
|
24052
|
-
className: "text-foreground break-all",
|
|
24053
|
-
children: formatValue(v)
|
|
24054
|
-
})
|
|
24055
|
-
] }, k))
|
|
24056
|
-
})
|
|
24057
|
-
]
|
|
24058
|
-
})
|
|
24059
|
-
]
|
|
24060
|
-
}, entry.id);
|
|
25219
|
+
!isLoading && capStates.size > 0 && filteredCapNames.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
25220
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25221
|
+
children: "No caps match the filter"
|
|
25222
|
+
}),
|
|
25223
|
+
filteredCapNames.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
25224
|
+
className: "py-1",
|
|
25225
|
+
children: filteredCapNames.map((capName) => {
|
|
25226
|
+
const state = capStates.get(capName);
|
|
25227
|
+
if (!state) return null;
|
|
25228
|
+
return /* @__PURE__ */ jsx(CapNode, {
|
|
25229
|
+
capName,
|
|
25230
|
+
slice: state.slice,
|
|
25231
|
+
updatedAt: state.updatedAt,
|
|
25232
|
+
searchNeedle,
|
|
25233
|
+
isPathOpen,
|
|
25234
|
+
onToggle: toggleNode
|
|
25235
|
+
}, capName);
|
|
24061
25236
|
})
|
|
24062
25237
|
})
|
|
24063
25238
|
]
|
|
24064
25239
|
})]
|
|
24065
25240
|
});
|
|
24066
25241
|
}
|
|
25242
|
+
/** Top-level tree node: one capability. Header carries the cap-name
|
|
25243
|
+
* pill + an "updated Ns ago" indicator; the body expands to the
|
|
25244
|
+
* slice's keys. */
|
|
25245
|
+
function CapNode({ capName, slice, updatedAt, searchNeedle, isPathOpen, onToggle }) {
|
|
25246
|
+
const path = capName;
|
|
25247
|
+
const open = isPathOpen(path, 0);
|
|
25248
|
+
const entries = Object.entries(slice);
|
|
25249
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
25250
|
+
className: "flex items-center gap-1.5 px-3 py-1 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25251
|
+
onClick: () => onToggle(path, open),
|
|
25252
|
+
children: [
|
|
25253
|
+
/* @__PURE__ */ jsx(Chevron, {
|
|
25254
|
+
open,
|
|
25255
|
+
hasChildren: entries.length > 0
|
|
25256
|
+
}),
|
|
25257
|
+
/* @__PURE__ */ jsx("span", {
|
|
25258
|
+
className: "inline-flex items-center gap-1 rounded-full border border-violet-500/30 bg-violet-500/15 text-violet-400 px-2 py-0.5 text-[9px] font-medium font-sans",
|
|
25259
|
+
children: capName
|
|
25260
|
+
}),
|
|
25261
|
+
/* @__PURE__ */ jsxs("span", {
|
|
25262
|
+
className: "text-[9px] text-foreground-subtle font-sans",
|
|
25263
|
+
children: [
|
|
25264
|
+
entries.length,
|
|
25265
|
+
" ",
|
|
25266
|
+
entries.length === 1 ? "key" : "keys"
|
|
25267
|
+
]
|
|
25268
|
+
}),
|
|
25269
|
+
/* @__PURE__ */ jsxs("span", {
|
|
25270
|
+
className: "ml-auto text-[9px] text-foreground-subtle/70 font-sans tabular-nums",
|
|
25271
|
+
children: ["updated ", formatAge(updatedAt)]
|
|
25272
|
+
})
|
|
25273
|
+
]
|
|
25274
|
+
}), open && /* @__PURE__ */ jsx("div", { children: entries.length === 0 ? /* @__PURE__ */ jsx(LeafEmpty, {
|
|
25275
|
+
depth: 1,
|
|
25276
|
+
label: "empty slice"
|
|
25277
|
+
}) : entries.map(([k, v]) => /* @__PURE__ */ jsx(TreeNode, {
|
|
25278
|
+
path: `${path}.${k}`,
|
|
25279
|
+
nodeKey: k,
|
|
25280
|
+
value: v,
|
|
25281
|
+
depth: 1,
|
|
25282
|
+
searchNeedle,
|
|
25283
|
+
isPathOpen,
|
|
25284
|
+
onToggle
|
|
25285
|
+
}, k)) })] });
|
|
25286
|
+
}
|
|
25287
|
+
/** Recursive tree node. A leaf renders `key: value`; an object or
|
|
25288
|
+
* array renders a collapsible branch whose children recurse one
|
|
25289
|
+
* level deeper. */
|
|
25290
|
+
function TreeNode({ path, nodeKey, value, depth, searchNeedle, isPathOpen, onToggle }) {
|
|
25291
|
+
const branch = asBranch(value);
|
|
25292
|
+
const indentStyle = { paddingLeft: `${12 + depth * 14}px` };
|
|
25293
|
+
if (!branch) return /* @__PURE__ */ jsxs("div", {
|
|
25294
|
+
className: "flex items-start gap-1 px-3 py-0.5",
|
|
25295
|
+
style: indentStyle,
|
|
25296
|
+
children: [
|
|
25297
|
+
/* @__PURE__ */ jsx("span", {
|
|
25298
|
+
className: cn("shrink-0", highlightCls(nodeKey, searchNeedle, "text-primary/70")),
|
|
25299
|
+
children: nodeKey
|
|
25300
|
+
}),
|
|
25301
|
+
/* @__PURE__ */ jsx("span", {
|
|
25302
|
+
className: "text-foreground-subtle",
|
|
25303
|
+
children: ":"
|
|
25304
|
+
}),
|
|
25305
|
+
/* @__PURE__ */ jsx("span", {
|
|
25306
|
+
className: cn("break-all", highlightCls(formatValue(value), searchNeedle, valueCls(value))),
|
|
25307
|
+
children: formatValue(value)
|
|
25308
|
+
})
|
|
25309
|
+
]
|
|
25310
|
+
});
|
|
25311
|
+
const open = isPathOpen(path, depth);
|
|
25312
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
25313
|
+
className: "flex items-center gap-1 px-3 py-0.5 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25314
|
+
style: indentStyle,
|
|
25315
|
+
onClick: () => onToggle(path, open),
|
|
25316
|
+
children: [
|
|
25317
|
+
/* @__PURE__ */ jsx(Chevron, {
|
|
25318
|
+
open,
|
|
25319
|
+
hasChildren: branch.entries.length > 0
|
|
25320
|
+
}),
|
|
25321
|
+
/* @__PURE__ */ jsx("span", {
|
|
25322
|
+
className: highlightCls(nodeKey, searchNeedle, "text-primary/70"),
|
|
25323
|
+
children: nodeKey
|
|
25324
|
+
}),
|
|
25325
|
+
/* @__PURE__ */ jsx("span", {
|
|
25326
|
+
className: "text-foreground-subtle/70 text-[9px]",
|
|
25327
|
+
children: branch.summary
|
|
25328
|
+
})
|
|
25329
|
+
]
|
|
25330
|
+
}), open && (branch.entries.length === 0 ? /* @__PURE__ */ jsx(LeafEmpty, {
|
|
25331
|
+
depth: depth + 1,
|
|
25332
|
+
label: branch.kind === "array" ? "empty array" : "empty object"
|
|
25333
|
+
}) : branch.entries.map(([k, v]) => /* @__PURE__ */ jsx(TreeNode, {
|
|
25334
|
+
path: `${path}.${k}`,
|
|
25335
|
+
nodeKey: k,
|
|
25336
|
+
value: v,
|
|
25337
|
+
depth: depth + 1,
|
|
25338
|
+
searchNeedle,
|
|
25339
|
+
isPathOpen,
|
|
25340
|
+
onToggle
|
|
25341
|
+
}, k)))] });
|
|
25342
|
+
}
|
|
25343
|
+
/** Placeholder row for an object/array/slice with no entries. */
|
|
25344
|
+
function LeafEmpty({ depth, label }) {
|
|
25345
|
+
return /* @__PURE__ */ jsx("div", {
|
|
25346
|
+
className: "px-3 py-0.5 text-foreground-subtle/60 italic",
|
|
25347
|
+
style: { paddingLeft: `${12 + depth * 14}px` },
|
|
25348
|
+
children: label
|
|
25349
|
+
});
|
|
25350
|
+
}
|
|
25351
|
+
/** Expand/collapse chevron. Renders a fixed-width spacer when the node
|
|
25352
|
+
* has no children so leaf and branch keys stay column-aligned. */
|
|
25353
|
+
function Chevron({ open, hasChildren }) {
|
|
25354
|
+
if (!hasChildren) return /* @__PURE__ */ jsx("span", { className: "inline-block w-3 shrink-0" });
|
|
25355
|
+
return open ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" });
|
|
25356
|
+
}
|
|
25357
|
+
/** Classify a value as an expandable branch (plain object or array)
|
|
25358
|
+
* or `null` for a leaf. Arrays expose index → element entries. */
|
|
25359
|
+
function asBranch(value) {
|
|
25360
|
+
if (Array.isArray(value)) return {
|
|
25361
|
+
kind: "array",
|
|
25362
|
+
entries: value.map((v, i) => [String(i), v]),
|
|
25363
|
+
summary: `[${value.length}]`
|
|
25364
|
+
};
|
|
25365
|
+
if (value && typeof value === "object") {
|
|
25366
|
+
const entries = Object.entries(value);
|
|
25367
|
+
return {
|
|
25368
|
+
kind: "object",
|
|
25369
|
+
entries,
|
|
25370
|
+
summary: `{${entries.length}}`
|
|
25371
|
+
};
|
|
25372
|
+
}
|
|
25373
|
+
return null;
|
|
25374
|
+
}
|
|
24067
25375
|
function formatValue(v) {
|
|
24068
25376
|
if (v === null) return "null";
|
|
24069
25377
|
if (v === void 0) return "undefined";
|
|
@@ -24075,9 +25383,46 @@ function formatValue(v) {
|
|
|
24075
25383
|
return String(v);
|
|
24076
25384
|
}
|
|
24077
25385
|
}
|
|
25386
|
+
/** Tailwind colour for a leaf value by JS type. */
|
|
25387
|
+
function valueCls(v) {
|
|
25388
|
+
if (v === null || v === void 0) return "text-foreground-subtle/60";
|
|
25389
|
+
if (typeof v === "number") return "text-amber-400";
|
|
25390
|
+
if (typeof v === "boolean") return v ? "text-emerald-400" : "text-rose-400";
|
|
25391
|
+
return "text-foreground";
|
|
25392
|
+
}
|
|
25393
|
+
/** Append a highlight background when `text` matches the live search
|
|
25394
|
+
* needle, so search hits are visible inside the tree. */
|
|
25395
|
+
function highlightCls(text, needle, base) {
|
|
25396
|
+
if (needle && text.toLowerCase().includes(needle)) return cn(base, "bg-violet-500/20 rounded px-0.5");
|
|
25397
|
+
return base;
|
|
25398
|
+
}
|
|
25399
|
+
/** Deep search a slice — true when `needle` appears in any key or any
|
|
25400
|
+
* stringified leaf value anywhere in the tree. */
|
|
25401
|
+
function sliceMatchesSearch(value, needle) {
|
|
25402
|
+
if (Array.isArray(value)) return value.some((v) => sliceMatchesSearch(v, needle));
|
|
25403
|
+
if (value && typeof value === "object") {
|
|
25404
|
+
for (const [k, v] of Object.entries(value)) {
|
|
25405
|
+
if (k.toLowerCase().includes(needle)) return true;
|
|
25406
|
+
if (sliceMatchesSearch(v, needle)) return true;
|
|
25407
|
+
}
|
|
25408
|
+
return false;
|
|
25409
|
+
}
|
|
25410
|
+
return formatValue(value).toLowerCase().includes(needle);
|
|
25411
|
+
}
|
|
25412
|
+
/** Human "Ns ago" / "Nm ago" age label for the per-cap freshness
|
|
25413
|
+
* indicator. */
|
|
25414
|
+
function formatAge(updatedAt) {
|
|
25415
|
+
const deltaMs = Date.now() - updatedAt;
|
|
25416
|
+
if (deltaMs < 5e3) return "just now";
|
|
25417
|
+
const secs = Math.floor(deltaMs / 1e3);
|
|
25418
|
+
if (secs < 60) return `${secs}s ago`;
|
|
25419
|
+
const mins = Math.floor(secs / 60);
|
|
25420
|
+
if (mins < 60) return `${mins}m ago`;
|
|
25421
|
+
return `${Math.floor(mins / 60)}h ago`;
|
|
25422
|
+
}
|
|
24078
25423
|
function ScopeBadge({ label, value }) {
|
|
24079
25424
|
return /* @__PURE__ */ jsxs("span", {
|
|
24080
|
-
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
|
|
25425
|
+
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium font-sans",
|
|
24081
25426
|
children: [
|
|
24082
25427
|
label,
|
|
24083
25428
|
": ",
|
|
@@ -24086,16 +25431,13 @@ function ScopeBadge({ label, value }) {
|
|
|
24086
25431
|
});
|
|
24087
25432
|
}
|
|
24088
25433
|
/**
|
|
24089
|
-
* Cap-name multiselect popover — same shape as
|
|
24090
|
-
*
|
|
24091
|
-
*
|
|
24092
|
-
* Caps don't have icons / styled badges like events do — we render
|
|
24093
|
-
* a flat name + violet-pill bracket so the look stays consistent
|
|
24094
|
-
* with the rest of the StateValuesStream rows.
|
|
25434
|
+
* Cap-name multiselect popover — same shape as `CategoryFilterPopover`
|
|
25435
|
+
* in event-stream.tsx (search box at top, Select All / None / Defaults
|
|
25436
|
+
* buttons, scrollable checkbox list).
|
|
24095
25437
|
*/
|
|
24096
25438
|
function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
|
|
24097
25439
|
return /* @__PURE__ */ jsxs("div", {
|
|
24098
|
-
className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col",
|
|
25440
|
+
className: "absolute z-20 top-full mt-1 left-0 w-72 max-h-80 rounded-md border border-border bg-surface shadow-lg flex flex-col font-sans",
|
|
24099
25441
|
onClick: (e) => e.stopPropagation(),
|
|
24100
25442
|
children: [
|
|
24101
25443
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -24166,24 +25508,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
|
|
|
24166
25508
|
]
|
|
24167
25509
|
});
|
|
24168
25510
|
}
|
|
24169
|
-
/**
|
|
24170
|
-
* Per-row Copy icon — same shape used in log-stream / event-stream
|
|
24171
|
-
* so the three live-streams' rows are visually symmetric. Stops
|
|
24172
|
-
* propagation so the click doesn't toggle the row's expand state.
|
|
24173
|
-
* Flashes a Check for ~1s on success.
|
|
24174
|
-
*/
|
|
24175
|
-
function RowCopyButton({ copied, onCopy }) {
|
|
24176
|
-
return /* @__PURE__ */ jsx("button", {
|
|
24177
|
-
type: "button",
|
|
24178
|
-
title: "Copy this state change",
|
|
24179
|
-
onClick: (e) => {
|
|
24180
|
-
e.stopPropagation();
|
|
24181
|
-
onCopy();
|
|
24182
|
-
},
|
|
24183
|
-
className: cn("rounded p-0.5 transition-colors mt-0.5", copied ? "text-emerald-400" : "text-foreground-subtle/60 hover:text-foreground hover:bg-surface-hover"),
|
|
24184
|
-
children: copied ? /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Copy, { className: "h-3 w-3" })
|
|
24185
|
-
});
|
|
24186
|
-
}
|
|
24187
25511
|
//#endregion
|
|
24188
25512
|
//#region src/composites/device-activity-panel.tsx
|
|
24189
25513
|
/**
|
|
@@ -24262,303 +25586,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
|
|
|
24262
25586
|
showFilters: false,
|
|
24263
25587
|
liveBuffer: logsBuffer
|
|
24264
25588
|
}),
|
|
24265
|
-
tab === "events" && /* @__PURE__ */ jsx(EventStream, {
|
|
24266
|
-
deviceId,
|
|
24267
|
-
defaultCategories: defaultEventCategories,
|
|
24268
|
-
maxHeight,
|
|
24269
|
-
showCategoryFilter: true,
|
|
24270
|
-
liveBuffer: eventsBuffer
|
|
24271
|
-
}),
|
|
24272
|
-
tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
|
|
24273
|
-
deviceId,
|
|
24274
|
-
defaultCaps: defaultStateCaps,
|
|
24275
|
-
maxHeight,
|
|
24276
|
-
liveBuffer: stateBuffer
|
|
24277
|
-
})
|
|
24278
|
-
]
|
|
24279
|
-
})]
|
|
24280
|
-
});
|
|
24281
|
-
}
|
|
24282
|
-
//#endregion
|
|
24283
|
-
//#region src/composites/confirm-action-button.tsx
|
|
24284
|
-
/**
|
|
24285
|
-
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
24286
|
-
*
|
|
24287
|
-
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
24288
|
-
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
24289
|
-
* normal Button that opens a Dialog; confirming runs the async action,
|
|
24290
|
-
* the trigger spinner-disables itself for the duration, and the dialog
|
|
24291
|
-
* closes on success. Errors surface inline at the dialog's bottom.
|
|
24292
|
-
*
|
|
24293
|
-
* Generic on `TResult` so callers don't have to discard the return
|
|
24294
|
-
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
24295
|
-
* for reboot, RefreshCw for restart).
|
|
24296
|
-
*/
|
|
24297
|
-
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
24298
|
-
const [open, setOpen] = useState(false);
|
|
24299
|
-
const [running, setRunning] = useState(false);
|
|
24300
|
-
const [error, setError] = useState(null);
|
|
24301
|
-
const handleConfirm = async () => {
|
|
24302
|
-
setError(null);
|
|
24303
|
-
setRunning(true);
|
|
24304
|
-
try {
|
|
24305
|
-
const result = await action();
|
|
24306
|
-
onSuccess?.(result);
|
|
24307
|
-
setOpen(false);
|
|
24308
|
-
} catch (err) {
|
|
24309
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
24310
|
-
} finally {
|
|
24311
|
-
setRunning(false);
|
|
24312
|
-
}
|
|
24313
|
-
};
|
|
24314
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
24315
|
-
type: "button",
|
|
24316
|
-
variant: triggerVariant,
|
|
24317
|
-
size,
|
|
24318
|
-
disabled,
|
|
24319
|
-
onClick: () => {
|
|
24320
|
-
setError(null);
|
|
24321
|
-
setOpen(true);
|
|
24322
|
-
},
|
|
24323
|
-
className,
|
|
24324
|
-
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
24325
|
-
}), /* @__PURE__ */ jsx(Dialog, {
|
|
24326
|
-
open,
|
|
24327
|
-
onOpenChange: (next) => !running && setOpen(next),
|
|
24328
|
-
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
24329
|
-
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
24330
|
-
className: "flex items-center gap-2",
|
|
24331
|
-
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
24332
|
-
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
24333
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
24334
|
-
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
24335
|
-
children: error
|
|
24336
|
-
}),
|
|
24337
|
-
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
24338
|
-
type: "button",
|
|
24339
|
-
variant: "ghost",
|
|
24340
|
-
size,
|
|
24341
|
-
disabled: running,
|
|
24342
|
-
onClick: () => setOpen(false),
|
|
24343
|
-
children: "Cancel"
|
|
24344
|
-
}), /* @__PURE__ */ jsxs(Button, {
|
|
24345
|
-
type: "button",
|
|
24346
|
-
variant: confirmVariant,
|
|
24347
|
-
size,
|
|
24348
|
-
disabled: running,
|
|
24349
|
-
onClick: () => {
|
|
24350
|
-
handleConfirm();
|
|
24351
|
-
},
|
|
24352
|
-
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
24353
|
-
})] })
|
|
24354
|
-
] })
|
|
24355
|
-
})] });
|
|
24356
|
-
}
|
|
24357
|
-
//#endregion
|
|
24358
|
-
//#region src/composites/ptz-overlay.tsx
|
|
24359
|
-
/**
|
|
24360
|
-
* PTZOverlay — pan / tilt / zoom controls.
|
|
24361
|
-
*
|
|
24362
|
-
* Two visual variants driven by `mode`:
|
|
24363
|
-
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
24364
|
-
* right of the camera viewport. Used as `extraOverlay` on the
|
|
24365
|
-
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
24366
|
-
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
24367
|
-
* legible against any frame.
|
|
24368
|
-
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
24369
|
-
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
24370
|
-
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
24371
|
-
* surrounding `bg-surface` so the dark theme reads consistently
|
|
24372
|
-
* instead of the previous always-dark-bubble look.
|
|
24373
|
-
*
|
|
24374
|
-
* Interaction model is identical across modes: short tap fires a
|
|
24375
|
-
* discrete pulse (`move`); long press starts continuous motion until
|
|
24376
|
-
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
24377
|
-
*/
|
|
24378
|
-
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
24379
|
-
const [pressedAt, setPressedAt] = useState(null);
|
|
24380
|
-
const [continuous, setContinuous] = useState(false);
|
|
24381
|
-
const handlePointerDown = useCallback((e) => {
|
|
24382
|
-
if (disabled) return;
|
|
24383
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
24384
|
-
setPressedAt(Date.now());
|
|
24385
|
-
setContinuous(false);
|
|
24386
|
-
const timer = setTimeout(() => {
|
|
24387
|
-
setContinuous(true);
|
|
24388
|
-
onStart(direction);
|
|
24389
|
-
}, 250);
|
|
24390
|
-
e.currentTarget.dataset.timer = String(timer);
|
|
24391
|
-
}, [
|
|
24392
|
-
direction,
|
|
24393
|
-
disabled,
|
|
24394
|
-
onStart
|
|
24395
|
-
]);
|
|
24396
|
-
const handlePointerUp = useCallback((e) => {
|
|
24397
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24398
|
-
if (timerId) clearTimeout(timerId);
|
|
24399
|
-
e.currentTarget.dataset.timer = "";
|
|
24400
|
-
if (continuous) onStop();
|
|
24401
|
-
else if (pressedAt !== null) onMove(direction);
|
|
24402
|
-
setPressedAt(null);
|
|
24403
|
-
setContinuous(false);
|
|
24404
|
-
}, [
|
|
24405
|
-
continuous,
|
|
24406
|
-
direction,
|
|
24407
|
-
onMove,
|
|
24408
|
-
onStop,
|
|
24409
|
-
pressedAt
|
|
24410
|
-
]);
|
|
24411
|
-
const handlePointerCancel = useCallback((e) => {
|
|
24412
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24413
|
-
if (timerId) clearTimeout(timerId);
|
|
24414
|
-
e.currentTarget.dataset.timer = "";
|
|
24415
|
-
if (continuous) onStop();
|
|
24416
|
-
setPressedAt(null);
|
|
24417
|
-
setContinuous(false);
|
|
24418
|
-
}, [continuous, onStop]);
|
|
24419
|
-
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
24420
|
-
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24421
|
-
return /* @__PURE__ */ jsx("button", {
|
|
24422
|
-
type: "button",
|
|
24423
|
-
disabled,
|
|
24424
|
-
onPointerDown: handlePointerDown,
|
|
24425
|
-
onPointerUp: handlePointerUp,
|
|
24426
|
-
onPointerCancel: handlePointerCancel,
|
|
24427
|
-
className: cn("flex items-center justify-center rounded transition-colors disabled:opacity-40", variant === "panel" ? "text-foreground hover:bg-surface-hover active:bg-surface-hover/80" : "text-white hover:bg-white/15 active:bg-white/25", sizeClass, className),
|
|
24428
|
-
title: direction,
|
|
24429
|
-
children: /* @__PURE__ */ jsx(Icon, { className: iconSizeClass })
|
|
24430
|
-
});
|
|
24431
|
-
}
|
|
24432
|
-
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
24433
|
-
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
|
|
24434
|
-
const [presetsOpen, setPresetsOpen] = useState(false);
|
|
24435
|
-
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
24436
|
-
const isPanel = mode === "panel";
|
|
24437
|
-
const containerClass = isPanel ? "flex flex-col items-stretch gap-2 w-full h-full p-3" : "pointer-events-auto absolute top-3 right-3 z-10 flex flex-col items-end gap-2";
|
|
24438
|
-
const rowClass = isPanel ? cn("flex items-center gap-3 rounded-lg p-2", busy && "ring-1 ring-primary/40") : cn("flex items-center gap-2 rounded-lg border border-white/30 bg-zinc-900/90 backdrop-blur-md p-2 shadow-lg shadow-black/40", busy && "ring-1 ring-primary/40");
|
|
24439
|
-
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
24440
|
-
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24441
|
-
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
24442
|
-
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
24443
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
24444
|
-
className: cn(containerClass, className),
|
|
24445
|
-
children: [error && /* @__PURE__ */ jsxs("div", {
|
|
24446
|
-
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
24447
|
-
children: ["PTZ: ", error]
|
|
24448
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
24449
|
-
className: cn(rowClass, isPanel && "justify-center"),
|
|
24450
|
-
children: [
|
|
24451
|
-
/* @__PURE__ */ jsxs("div", {
|
|
24452
|
-
className: "grid grid-cols-3 gap-0.5",
|
|
24453
|
-
children: [
|
|
24454
|
-
/* @__PURE__ */ jsx("span", {
|
|
24455
|
-
"aria-hidden": true,
|
|
24456
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24457
|
-
}),
|
|
24458
|
-
/* @__PURE__ */ jsx(DPadButton, {
|
|
24459
|
-
direction: "up",
|
|
24460
|
-
icon: ArrowUp,
|
|
24461
|
-
variant: mode,
|
|
24462
|
-
onMove: move,
|
|
24463
|
-
onStart: startContinuous,
|
|
24464
|
-
onStop: stopContinuous
|
|
24465
|
-
}),
|
|
24466
|
-
/* @__PURE__ */ jsx("span", {
|
|
24467
|
-
"aria-hidden": true,
|
|
24468
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24469
|
-
}),
|
|
24470
|
-
/* @__PURE__ */ jsx(DPadButton, {
|
|
24471
|
-
direction: "left",
|
|
24472
|
-
icon: ArrowLeft,
|
|
24473
|
-
variant: mode,
|
|
24474
|
-
onMove: move,
|
|
24475
|
-
onStart: startContinuous,
|
|
24476
|
-
onStop: stopContinuous
|
|
24477
|
-
}),
|
|
24478
|
-
/* @__PURE__ */ jsx("span", {
|
|
24479
|
-
"aria-hidden": true,
|
|
24480
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24481
|
-
}),
|
|
24482
|
-
/* @__PURE__ */ jsx(DPadButton, {
|
|
24483
|
-
direction: "right",
|
|
24484
|
-
icon: ArrowRight,
|
|
24485
|
-
variant: mode,
|
|
24486
|
-
onMove: move,
|
|
24487
|
-
onStart: startContinuous,
|
|
24488
|
-
onStop: stopContinuous
|
|
24489
|
-
}),
|
|
24490
|
-
/* @__PURE__ */ jsx("span", {
|
|
24491
|
-
"aria-hidden": true,
|
|
24492
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24493
|
-
}),
|
|
24494
|
-
/* @__PURE__ */ jsx(DPadButton, {
|
|
24495
|
-
direction: "down",
|
|
24496
|
-
icon: ArrowDown,
|
|
24497
|
-
variant: mode,
|
|
24498
|
-
onMove: move,
|
|
24499
|
-
onStart: startContinuous,
|
|
24500
|
-
onStop: stopContinuous
|
|
24501
|
-
}),
|
|
24502
|
-
/* @__PURE__ */ jsx("span", {
|
|
24503
|
-
"aria-hidden": true,
|
|
24504
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24505
|
-
})
|
|
24506
|
-
]
|
|
24507
|
-
}),
|
|
24508
|
-
(showZoom || showHome) && /* @__PURE__ */ jsx("div", { className: sepClass }),
|
|
24509
|
-
/* @__PURE__ */ jsxs("div", {
|
|
24510
|
-
className: "flex flex-col gap-0.5",
|
|
24511
|
-
children: [showZoom && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
24512
|
-
type: "button",
|
|
24513
|
-
onClick: () => zoom("in"),
|
|
24514
|
-
disabled: busy,
|
|
24515
|
-
title: "Zoom in",
|
|
24516
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24517
|
-
children: /* @__PURE__ */ jsx(ZoomIn, { className: sideIconSize })
|
|
24518
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
24519
|
-
type: "button",
|
|
24520
|
-
onClick: () => zoom("out"),
|
|
24521
|
-
disabled: busy,
|
|
24522
|
-
title: "Zoom out",
|
|
24523
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24524
|
-
children: /* @__PURE__ */ jsx(ZoomOut, { className: sideIconSize })
|
|
24525
|
-
})] }), showHome && /* @__PURE__ */ jsx("button", {
|
|
24526
|
-
type: "button",
|
|
24527
|
-
onClick: () => goHome(),
|
|
24528
|
-
disabled: busy,
|
|
24529
|
-
title: "Go home",
|
|
24530
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24531
|
-
children: /* @__PURE__ */ jsx(House, { className: sideIconSize })
|
|
24532
|
-
})]
|
|
25589
|
+
tab === "events" && /* @__PURE__ */ jsx(EventStream, {
|
|
25590
|
+
deviceId,
|
|
25591
|
+
defaultCategories: defaultEventCategories,
|
|
25592
|
+
maxHeight,
|
|
25593
|
+
showCategoryFilter: true,
|
|
25594
|
+
liveBuffer: eventsBuffer
|
|
24533
25595
|
}),
|
|
24534
|
-
|
|
24535
|
-
|
|
24536
|
-
|
|
24537
|
-
|
|
24538
|
-
|
|
24539
|
-
disabled: busy,
|
|
24540
|
-
className: cn("flex items-center gap-1 rounded px-2 text-[10px] disabled:opacity-40", isPanel ? "h-9 text-foreground hover:bg-surface-hover" : "h-7 text-white hover:bg-white/15"),
|
|
24541
|
-
title: "Presets",
|
|
24542
|
-
children: ["Presets", /* @__PURE__ */ jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
24543
|
-
}), presetsOpen && /* @__PURE__ */ jsx("div", {
|
|
24544
|
-
className: cn("absolute right-0 min-w-[140px] rounded-lg shadow-xl py-1 z-20", isPanel ? "bottom-full mb-1 bg-surface border border-border" : "top-full mt-1 border border-white/30 bg-zinc-900/95 backdrop-blur-md"),
|
|
24545
|
-
children: presets.map((p) => /* @__PURE__ */ jsx("button", {
|
|
24546
|
-
type: "button",
|
|
24547
|
-
onClick: () => {
|
|
24548
|
-
goToPreset(p.id);
|
|
24549
|
-
setPresetsOpen(false);
|
|
24550
|
-
},
|
|
24551
|
-
disabled: busy,
|
|
24552
|
-
className: cn("block w-full px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15"),
|
|
24553
|
-
children: p.name || p.id
|
|
24554
|
-
}, p.id))
|
|
24555
|
-
})]
|
|
25596
|
+
tab === "state" && /* @__PURE__ */ jsx(StateValuesStream, {
|
|
25597
|
+
deviceId,
|
|
25598
|
+
defaultCaps: defaultStateCaps,
|
|
25599
|
+
maxHeight,
|
|
25600
|
+
liveBuffer: stateBuffer
|
|
24556
25601
|
})
|
|
24557
25602
|
]
|
|
24558
25603
|
})]
|
|
24559
25604
|
});
|
|
24560
25605
|
}
|
|
24561
25606
|
//#endregion
|
|
25607
|
+
//#region src/composites/confirm-action-button.tsx
|
|
25608
|
+
/**
|
|
25609
|
+
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
25610
|
+
*
|
|
25611
|
+
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
25612
|
+
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
25613
|
+
* normal Button that opens a Dialog; confirming runs the async action,
|
|
25614
|
+
* the trigger spinner-disables itself for the duration, and the dialog
|
|
25615
|
+
* closes on success. Errors surface inline at the dialog's bottom.
|
|
25616
|
+
*
|
|
25617
|
+
* Generic on `TResult` so callers don't have to discard the return
|
|
25618
|
+
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
25619
|
+
* for reboot, RefreshCw for restart).
|
|
25620
|
+
*/
|
|
25621
|
+
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
25622
|
+
const [open, setOpen] = useState(false);
|
|
25623
|
+
const [running, setRunning] = useState(false);
|
|
25624
|
+
const [error, setError] = useState(null);
|
|
25625
|
+
const handleConfirm = async () => {
|
|
25626
|
+
setError(null);
|
|
25627
|
+
setRunning(true);
|
|
25628
|
+
try {
|
|
25629
|
+
const result = await action();
|
|
25630
|
+
onSuccess?.(result);
|
|
25631
|
+
setOpen(false);
|
|
25632
|
+
} catch (err) {
|
|
25633
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
25634
|
+
} finally {
|
|
25635
|
+
setRunning(false);
|
|
25636
|
+
}
|
|
25637
|
+
};
|
|
25638
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
25639
|
+
type: "button",
|
|
25640
|
+
variant: triggerVariant,
|
|
25641
|
+
size,
|
|
25642
|
+
disabled,
|
|
25643
|
+
onClick: () => {
|
|
25644
|
+
setError(null);
|
|
25645
|
+
setOpen(true);
|
|
25646
|
+
},
|
|
25647
|
+
className,
|
|
25648
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
25649
|
+
}), /* @__PURE__ */ jsx(Dialog, {
|
|
25650
|
+
open,
|
|
25651
|
+
onOpenChange: (next) => !running && setOpen(next),
|
|
25652
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { children: [
|
|
25653
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsxs(DialogTitle, {
|
|
25654
|
+
className: "flex items-center gap-2",
|
|
25655
|
+
children: [/* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
25656
|
+
}), typeof description === "string" ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : description] }),
|
|
25657
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
25658
|
+
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
25659
|
+
children: error
|
|
25660
|
+
}),
|
|
25661
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
25662
|
+
type: "button",
|
|
25663
|
+
variant: "ghost",
|
|
25664
|
+
size,
|
|
25665
|
+
disabled: running,
|
|
25666
|
+
onClick: () => setOpen(false),
|
|
25667
|
+
children: "Cancel"
|
|
25668
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
25669
|
+
type: "button",
|
|
25670
|
+
variant: confirmVariant,
|
|
25671
|
+
size,
|
|
25672
|
+
disabled: running,
|
|
25673
|
+
onClick: () => {
|
|
25674
|
+
handleConfirm();
|
|
25675
|
+
},
|
|
25676
|
+
children: [running && /* @__PURE__ */ jsx(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
25677
|
+
})] })
|
|
25678
|
+
] })
|
|
25679
|
+
})] });
|
|
25680
|
+
}
|
|
25681
|
+
//#endregion
|
|
24562
25682
|
//#region src/composites/snapshot-button.tsx
|
|
24563
25683
|
/**
|
|
24564
25684
|
* SnapshotButton — operator-facing manual snapshot trigger.
|
|
@@ -25465,169 +26585,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
|
|
|
25465
26585
|
};
|
|
25466
26586
|
}
|
|
25467
26587
|
//#endregion
|
|
25468
|
-
//#region src/hooks/use-ptz.ts
|
|
25469
|
-
/**
|
|
25470
|
-
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
25471
|
-
*
|
|
25472
|
-
* Wraps the `ptz` capability methods through the canonical
|
|
25473
|
-
* `useDeviceProxy` surface — every call goes through
|
|
25474
|
-
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
25475
|
-
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
25476
|
-
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
25477
|
-
*
|
|
25478
|
-
* Returns:
|
|
25479
|
-
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
25480
|
-
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
25481
|
-
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
25482
|
-
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
25483
|
-
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
25484
|
-
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
25485
|
-
* - `goHome()`: jump to preset 0.
|
|
25486
|
-
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
25487
|
-
*
|
|
25488
|
-
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
25489
|
-
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
25490
|
-
* addon pages (AddonPageProps.trpc) alike.
|
|
25491
|
-
*/
|
|
25492
|
-
var DIRECTION_VECTORS = {
|
|
25493
|
-
"up": {
|
|
25494
|
-
pan: 0,
|
|
25495
|
-
tilt: 1
|
|
25496
|
-
},
|
|
25497
|
-
"down": {
|
|
25498
|
-
pan: 0,
|
|
25499
|
-
tilt: -1
|
|
25500
|
-
},
|
|
25501
|
-
"left": {
|
|
25502
|
-
pan: -1,
|
|
25503
|
-
tilt: 0
|
|
25504
|
-
},
|
|
25505
|
-
"right": {
|
|
25506
|
-
pan: 1,
|
|
25507
|
-
tilt: 0
|
|
25508
|
-
},
|
|
25509
|
-
"up-left": {
|
|
25510
|
-
pan: -1,
|
|
25511
|
-
tilt: 1
|
|
25512
|
-
},
|
|
25513
|
-
"up-right": {
|
|
25514
|
-
pan: 1,
|
|
25515
|
-
tilt: 1
|
|
25516
|
-
},
|
|
25517
|
-
"down-left": {
|
|
25518
|
-
pan: -1,
|
|
25519
|
-
tilt: -1
|
|
25520
|
-
},
|
|
25521
|
-
"down-right": {
|
|
25522
|
-
pan: 1,
|
|
25523
|
-
tilt: -1
|
|
25524
|
-
}
|
|
25525
|
-
};
|
|
25526
|
-
function usePTZ(trpc, deviceId, options) {
|
|
25527
|
-
const defaultSpeed = options?.defaultSpeed ?? .5;
|
|
25528
|
-
const pulseMs = options?.pulseMs ?? 250;
|
|
25529
|
-
const enabled = options?.enabled ?? true;
|
|
25530
|
-
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
25531
|
-
const [presets, setPresets] = useState([]);
|
|
25532
|
-
const [busy, setBusy] = useState(false);
|
|
25533
|
-
const [error, setError] = useState(null);
|
|
25534
|
-
const refreshPresets = useCallback(async () => {
|
|
25535
|
-
if (!enabled || !ptz) return;
|
|
25536
|
-
try {
|
|
25537
|
-
setPresets(await ptz.getPresets({}));
|
|
25538
|
-
} catch (err) {
|
|
25539
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
25540
|
-
if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
|
|
25541
|
-
setError(msg);
|
|
25542
|
-
}
|
|
25543
|
-
}, [ptz, enabled]);
|
|
25544
|
-
useEffect(() => {
|
|
25545
|
-
refreshPresets();
|
|
25546
|
-
}, [refreshPresets]);
|
|
25547
|
-
const wrap = useCallback(async (fn) => {
|
|
25548
|
-
if (!enabled || !ptz) return void 0;
|
|
25549
|
-
setError(null);
|
|
25550
|
-
setBusy(true);
|
|
25551
|
-
try {
|
|
25552
|
-
return await fn();
|
|
25553
|
-
} catch (err) {
|
|
25554
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
25555
|
-
return;
|
|
25556
|
-
} finally {
|
|
25557
|
-
setBusy(false);
|
|
25558
|
-
}
|
|
25559
|
-
}, [enabled, ptz]);
|
|
25560
|
-
return {
|
|
25561
|
-
move: useCallback(async (direction, speed) => {
|
|
25562
|
-
if (!ptz) return;
|
|
25563
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25564
|
-
const s = speed ?? defaultSpeed;
|
|
25565
|
-
await wrap(async () => {
|
|
25566
|
-
await ptz.move({
|
|
25567
|
-
pan: v.pan,
|
|
25568
|
-
tilt: v.tilt,
|
|
25569
|
-
speed: s
|
|
25570
|
-
});
|
|
25571
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25572
|
-
await ptz.stop({});
|
|
25573
|
-
});
|
|
25574
|
-
}, [
|
|
25575
|
-
ptz,
|
|
25576
|
-
defaultSpeed,
|
|
25577
|
-
pulseMs,
|
|
25578
|
-
wrap
|
|
25579
|
-
]),
|
|
25580
|
-
startContinuous: useCallback(async (direction, speed) => {
|
|
25581
|
-
if (!ptz) return;
|
|
25582
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25583
|
-
const s = speed ?? defaultSpeed;
|
|
25584
|
-
await wrap(() => ptz.continuousMove({
|
|
25585
|
-
pan: v.pan,
|
|
25586
|
-
tilt: v.tilt,
|
|
25587
|
-
speed: s
|
|
25588
|
-
}));
|
|
25589
|
-
}, [
|
|
25590
|
-
ptz,
|
|
25591
|
-
defaultSpeed,
|
|
25592
|
-
wrap
|
|
25593
|
-
]),
|
|
25594
|
-
stopContinuous: useCallback(async () => {
|
|
25595
|
-
if (!ptz) return;
|
|
25596
|
-
await wrap(() => ptz.stop({}));
|
|
25597
|
-
}, [ptz, wrap]),
|
|
25598
|
-
zoom: useCallback(async (direction, speed) => {
|
|
25599
|
-
if (!ptz) return;
|
|
25600
|
-
const z = direction === "in" ? 1 : -1;
|
|
25601
|
-
const s = speed ?? defaultSpeed;
|
|
25602
|
-
await wrap(async () => {
|
|
25603
|
-
await ptz.move({
|
|
25604
|
-
zoom: z,
|
|
25605
|
-
speed: s
|
|
25606
|
-
});
|
|
25607
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25608
|
-
await ptz.stop({});
|
|
25609
|
-
});
|
|
25610
|
-
}, [
|
|
25611
|
-
ptz,
|
|
25612
|
-
defaultSpeed,
|
|
25613
|
-
pulseMs,
|
|
25614
|
-
wrap
|
|
25615
|
-
]),
|
|
25616
|
-
goHome: useCallback(async () => {
|
|
25617
|
-
if (!ptz) return;
|
|
25618
|
-
await wrap(() => ptz.goHome({}));
|
|
25619
|
-
}, [ptz, wrap]),
|
|
25620
|
-
goToPreset: useCallback(async (presetId) => {
|
|
25621
|
-
if (!ptz) return;
|
|
25622
|
-
await wrap(() => ptz.goToPreset({ presetId }));
|
|
25623
|
-
}, [ptz, wrap]),
|
|
25624
|
-
presets,
|
|
25625
|
-
refreshPresets,
|
|
25626
|
-
busy,
|
|
25627
|
-
error
|
|
25628
|
-
};
|
|
25629
|
-
}
|
|
25630
|
-
//#endregion
|
|
25631
26588
|
//#region src/hooks/use-doorbell-events.ts
|
|
25632
26589
|
/**
|
|
25633
26590
|
* useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
|
|
@@ -25849,6 +26806,6 @@ function useEventInvalidation(queryKey, categories) {
|
|
|
25849
26806
|
]);
|
|
25850
26807
|
}
|
|
25851
26808
|
//#endregion
|
|
25852
|
-
export { AddonGlobalSettingsForm, AgentStepEditor, AppShell, AudioClassificationList, AudioLevelWaveform, AudioWaveform, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, Button, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, CameraStreamPlayer, Card, Checkbox, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, CopyButton, CustomFieldRenderersProvider, DEFAULT_COLOR, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, EmptyState, ErrorBox, EventStream, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, INPUT_COMPACT, IconButton, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LogStream, LoginForm, MobileDrawer, NodeMultiSelectField, NodePicker, NodeSelectField, PHASE_CONFIG, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverTrigger, ProviderBadge, QrCode, ResponseLog, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, ScopePicker, ScrollArea, Select, SemanticBadge, Separator, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SnapshotButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, StreamBrokerSelector, StreamPanel, Switch, SystemProvider, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, Tabs, TabsContent, TabsList, TabsTrigger, ThemeProvider, Tooltip, TooltipContent, TooltipTrigger, VersionBadge, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, buildStepTreeFromSchema, cn, createSharedContext, createTheme, darkColors, defaultTheme, deriveDeviceKind, ensureMfHostInit, getClassColor, getPhaseVisual, isFieldVisible, lightColors, mirror, mountAddonPage, providerIcons, statusIcons, themeToCss, trpc, useAccessoriesGetStatus, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsUninstallPackage, useAddonsUpdateFrameworkPackage, useAddonsUpdatePackage, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerClassify, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useBackupDelete, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBatteryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetRtspEntries, useClusterNodes, useConfirm, useCustomFieldRenderer, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetInfo, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceBattery, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAllocateDeviceId, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverDevices, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetChildren, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerSetDisabled, useDeviceManagerSetLocation, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceOpsGetConfigEntries, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetStatus, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellEvents, useDoorbellGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFeatureProbeGetStatus, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkPcm, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLiveBuffer, useLiveEvent, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetPreferred, useLocalNetworkList, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkSetAllowedAddresses, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetProcessStats, useMetricsProviderKillProcess, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetStatus, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputSend, useNotificationOutputSendTest, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdSetOverlay, usePTZ, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsClearTracks, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsListTracks, usePipelineExecutorCacheFrameInPool, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDetect, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorReprobeEngine, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignDecoder, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDecoderAssignment, usePipelineOrchestratorGetDecoderAssignments, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorRebalance, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentAddonDefaults, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignDecoder, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerReportMotion, usePlatformProbeGetCapabilities, usePlatformProbeGetHardware, usePlatformProbeGetHardwareEncoders, usePlatformProbeRefreshHardwareEncoders, usePlatformProbeResolveHwAccel, usePlatformProbeResolveInferenceConfig, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzContinuousMove, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzStop, useRebootReboot, useRecordingEngineDisable, useRecordingEngineEnable, useRecordingEngineEstimateGlobalStorage, useRecordingEngineEstimateStorage, useRecordingEngineGetAvailability, useRecordingEngineGetConfig, useRecordingEngineGetMotionStats, useRecordingEngineGetPlaylist, useRecordingEngineGetPolicy, useRecordingEngineGetPolicyStatus, useRecordingEngineGetRetentionConfig, useRecordingEngineGetSegments, useRecordingEngineGetStatus, useRecordingEngineGetStorageUsage, useRecordingEngineGetThumbnail, useRecordingEngineSetPolicy, useRecordingEngineUpdateConfig, useRecordingEngineUpdateRetentionConfig, useRecordingGetPlaybackUrl, useRecordingGetSegments, useRecordingGetThumbnailAt, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreGet, useSettingsStoreInsert, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetStatus, useSnapshotInvalidateCache, useSnapshotProviderGetSnapshot, useSnapshotProviderSupportsDevice, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageGetDefaultLocation, useStorageList, useStorageListLocations, useStorageListProviders, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBroker, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerPublishCameraStream, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerUnassignProfile, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetRetentionConfig, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetRetentionConfig, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, useUserManagementConfirmTotp, useUserManagementCreateApiKey, useUserManagementCreateScopedToken, useUserManagementCreateUser, useUserManagementDeleteUser, useUserManagementDisableTotp, useUserManagementGetTotpStatus, useUserManagementListApiKeys, useUserManagementListScopedTokens, useUserManagementListUsers, useUserManagementResetPassword, useUserManagementRevokeApiKey, useUserManagementRevokeScopedToken, useUserManagementSetUserScopes, useUserManagementSetupTotp, useUserManagementUpdateUser, useUserManagementValidateApiKey, useUserManagementValidateCredentials, useUserManagementValidateScopedToken, useUserManagementVerifyTotp, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, validateScopes };
|
|
26809
|
+
export { AddonGlobalSettingsForm, AgentStepEditor, AppShell, AudioClassificationList, AudioLevelWaveform, AudioWaveform, AutotrackSection, BTN_COMPACT, BTN_COMPACT_DANGER, BTN_COMPACT_PRIMARY, BTN_COMPACT_WARNING, Badge, BatteryBadge, BottomSheet, Breadcrumb, Button, CHIP_ACTIVE, CHIP_BASE, CHIP_INACTIVE, CLASS_COLORS, CameraStreamPlayer, Card, Checkbox, CodeBlock, CollapsibleCard, ConfigFormBuilder, FormField as ConfigFormField, ConfigSchemaField, ConfirmActionButton, ConfirmDialogProvider, CopyButton, CustomFieldRenderersProvider, DEFAULT_COLOR, DataTable, DetectionCanvas, DetectionOverlay, DetectionResultTree, DevShell, DeviceActivityPanel, DeviceCard, DeviceContextProvider, DeviceExportPanel, DeviceGrid, DeviceItem, DeviceList, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DiscoveryPanel, DoorbellRecentPanel, Dropdown, DropdownContent, DropdownItem, DropdownTrigger, EmptyState, ErrorBox, EventStream, FilterBar, FloatingEventStream, FloatingLogStream, FloatingPanel, FormField$1 as FormField, GRID_GAP, GRID_PAIRED, GRID_QUICK_STATS, HOST_WIDGETS, INPUT_COMPACT, IconButton, ImageSelector, InferenceConfigSelector, Input, KebabMenu, KeyValueList, LIST_ROW, Label, LogStream, LoginForm, MobileDrawer, MotionZonesSettings, NodeMultiSelectField, NodePicker, NodeSelectField, PHASE_CONFIG, PTZOverlay, PageHeader, PhaseIcon, PipelineBuilder, PipelineRuntimeSelector, PipelineStep, PipelineTreeMatrix, PlayerOverlaysProvider, Popover, PopoverContent, PopoverTrigger, ProviderBadge, PtzPanel, QrCode, ResponseLog, SECTION_BODY, SECTION_CARD, SECTION_HEADER, SPLIT_PANEL_OUTER, SPLIT_PANEL_SIDE, STACK_GAP, ScopePicker, ScrollArea, Select, SemanticBadge, Separator, Sidebar, SidebarItem, Skeleton, SlideOverPanel, SnapshotButton, StatCard, StateValuesStream, StatusBadge, StepTimings, StepTreeMaster, StreamBrokerSelector, StreamPanel, Switch, SystemProvider, TEXT_FIELD_LABEL, TEXT_HINT, TEXT_METRIC, TEXT_SECTION_LABEL, TEXT_VALUE, Tabs, TabsContent, TabsList, TabsTrigger, ThemeProvider, Tooltip, TooltipContent, TooltipTrigger, VersionBadge, WidgetRegistryProvider, WidgetSlot, ZoneEditingProvider, buildStepTreeFromSchema, cn, createSharedContext, createTheme, darkColors, defaultTheme, deriveDeviceKind, ensureMfHostInit, getClassColor, getPhaseVisual, isFieldVisible, lightColors, loadRemoteBundle, mirror, mountAddonPage, providerIcons, statusIcons, themeToCss, trpc, useAccessoriesGetStatus, useAddonPagesListPages, useAddonSettingsGetDeviceSettings, useAddonSettingsGetGlobalSettings, useAddonSettingsUpdateDeviceSettings, useAddonSettingsUpdateGlobalSettings, useAddonWidgetsListWidgets, useAddonsApplyAutoUpdateToAll, useAddonsCustom, useAddonsForceRefresh, useAddonsGetAddonAutoUpdate, useAddonsGetAutoUpdateSettings, useAddonsGetLastRestart, useAddonsGetLogs, useAddonsGetVersions, useAddonsInstallFromWorkspace, useAddonsInstallPackage, useAddonsIsWorkspaceAvailable, useAddonsList, useAddonsListCapabilityProviders, useAddonsListFrameworkPackages, useAddonsListPackages, useAddonsListUpdates, useAddonsListWorkspacePackages, useAddonsOnAddonLogs, useAddonsReloadPackages, useAddonsRestartAddon, useAddonsRestartServer, useAddonsRetryLoad, useAddonsRollbackPackage, useAddonsSearchAvailable, useAddonsSetAddonAutoUpdate, useAddonsSetAutoUpdateSettings, useAddonsSetCapabilityProviderEnabled, useAddonsUninstallPackage, useAddonsUpdateFrameworkPackage, useAddonsUpdatePackage, useAlertsDismiss, useAlertsEmit, useAlertsGetUnreadCount, useAlertsList, useAlertsMarkAllRead, useAlertsMarkRead, useAlertsUpdate, useAllWidgets, useAudioAnalysisApplyDeviceSettingsPatch, useAudioAnalysisGetDeviceLiveContribution, useAudioAnalysisGetDeviceSettingsContribution, useAudioAnalysisResolveDeviceSettings, useAudioAnalyzerAnalyseChunk, useAudioAnalyzerClassify, useAudioAnalyzerDispose, useAudioAnalyzerIsReady, useAudioAnalyzerReprobeAudioEngine, useAudioCodecCanHandle, useAudioCodecCloseSession, useAudioCodecCreateDecodeSession, useAudioCodecCreateEncodeSession, useAudioCodecFlushEncode, useAudioCodecListActiveSessions, useAudioCodecListSupportedCodecs, useAudioCodecPullEncoded, useAudioCodecPullPcm, useAudioCodecPushEncodedFrame, useAudioCodecPushPcm, useAudioMetricsGetCurrentSnapshot, useAudioMetricsGetHistory, useBackupDelete, useBackupGetEntries, useBackupList, useBackupListArchives, useBackupListDestinations, useBackupListLocations, useBackupPreviewSchedule, useBackupRestore, useBackupTrigger, useBackupUpsertDestinationPolicy, useBatteryGetStatus, useBrightnessGetStatus, useBrightnessSetBrightness, useCameraCredentialsGetCredentials, useCameraCredentialsGetStatus, useCameraStreamsGetBrokerStreams, useCameraStreamsGetCameraStreams, useCameraStreamsGetRtspEntries, useClusterNodes, useConfirm, useCustomFieldRenderer, useDebouncedString, useDecoderCreateSession, useDecoderDestroySession, useDecoderGetFrame, useDecoderGetInfo, useDecoderGetShmStats, useDecoderGetStats, useDecoderListActiveSessions, useDecoderOpenStream, useDecoderPullFrames, useDecoderPullHandles, useDecoderPushPacket, useDecoderReprobeHwaccel, useDecoderSupportsCodec, useDecoderUpdateConfig, useDetectionPipelineApplyDeviceSettingsPatch, useDetectionPipelineGetDeviceLiveContribution, useDetectionPipelineGetDeviceSettingsContribution, useDevShell, useDevice, useDeviceAutotrack, useDeviceBattery, useDeviceCapability, useDeviceDetections, useDeviceDiscoveryAdoptDevice, useDeviceDiscoveryGetStatus, useDeviceDiscoveryListDiscovered, useDeviceDiscoveryRefreshDiscovery, useDeviceDiscoveryReleaseDevice, useDeviceExportApplyDeviceSettingsPatch, useDeviceExportExposeDevice, useDeviceExportGetDeviceLiveContribution, useDeviceExportGetDeviceSettingsContribution, useDeviceExportGetStatus, useDeviceExportListExposedDevices, useDeviceExportListSupportedDeviceKinds, useDeviceExportUnexposeDevice, useDeviceId, useDeviceManagerAddLocation, useDeviceManagerAdoptDevice, useDeviceManagerAllocateDeviceId, useDeviceManagerCreateDevice, useDeviceManagerDisable, useDeviceManagerDiscoverDevices, useDeviceManagerEnable, useDeviceManagerGetAllBindings, useDeviceManagerGetBindings, useDeviceManagerGetChildren, useDeviceManagerGetConfigSchema, useDeviceManagerGetCreationSchema, useDeviceManagerGetDevice, useDeviceManagerGetDeviceAggregate, useDeviceManagerGetDeviceLiveInfoAggregate, useDeviceManagerGetDeviceSettingsAggregate, useDeviceManagerGetDeviceStatusAggregate, useDeviceManagerGetSettingsSchema, useDeviceManagerGetStreamProfileMap, useDeviceManagerGetStreamSources, useDeviceManagerListAll, useDeviceManagerListBindableCapsForDeviceType, useDeviceManagerListLocations, useDeviceManagerListPersistedByAddon, useDeviceManagerListWrappersForCap, useDeviceManagerLoadConfig, useDeviceManagerLoadMeta, useDeviceManagerLoadRuntimeState, useDeviceManagerPersistConfig, useDeviceManagerProbeStreams, useDeviceManagerRegisterDevice, useDeviceManagerRemove, useDeviceManagerRemoveDevice, useDeviceManagerRemoveLocation, useDeviceManagerSetDisabled, useDeviceManagerSetLocation, useDeviceManagerSetMetadata, useDeviceManagerSetName, useDeviceManagerSetStreamProfileMap, useDeviceManagerSetWrapperActive, useDeviceManagerTestCreationField, useDeviceManagerTestField, useDeviceManagerUpdateConfig, useDeviceManagerUpdateDeviceField, useDeviceManagerUpdateDeviceFieldsBatch, useDeviceOpsGetConfigEntries, useDeviceOpsGetSettingsSchema, useDeviceOpsGetStreamSources, useDeviceOpsRemoveDevice, useDeviceOpsSetConfig, useDeviceProviderAdoptDiscoveredDevice, useDeviceProviderCreateDevice, useDeviceProviderDiscoverDevices, useDeviceProviderGetChildCreationSchema, useDeviceProviderGetDevices, useDeviceProviderGetStatus, useDeviceProviderStart, useDeviceProviderStop, useDeviceProviderSupportsDiscovery, useDeviceProviderSupportsManualCreation, useDeviceProviderTestCreationField, useDeviceProxy, useDeviceSnapshot, useDeviceSnapshotImage, useDeviceState, useDeviceStateGetAllSnapshots, useDeviceStateGetCapSlice, useDeviceStateGetSnapshot, useDeviceStateSetCapSlice, useDeviceStateSlice, useDeviceStatusGetStatus, useDeviceWebrtc, useDevices, useDoorbellEvents, useDoorbellGetStatus, useEventInvalidation, useEventStreamLatest, useEventStreamMap, useEventsGetEventClipUrl, useEventsGetEventThumbnail, useEventsGetEvents, useFeatureProbeGetStatus, useIntegrationsCreate, useIntegrationsDelete, useIntegrationsGet, useIntegrationsGetAvailableTypes, useIntegrationsGetByAddonId, useIntegrationsGetSettings, useIntegrationsList, useIntegrationsSetSettings, useIntegrationsTestConnection, useIntegrationsUpdate, useIntercomEndTalkSession, useIntercomGetStatus, useIntercomHandleAnswer, useIntercomPushTalkPcm, useIntercomStartSession, useIntercomStartTalkSession, useIntercomStopSession, useIsMidWidth, useIsMobile, useLiveBuffer, useLiveEvent, useLocalNetworkGetAllowedAddresses, useLocalNetworkGetConnectionEndpoints, useLocalNetworkGetPreferred, useLocalNetworkList, useLocalNetworkResetAllowlistToBestMatch, useLocalNetworkSetAllowedAddresses, useMeshNetworkGetStatus, useMeshNetworkJoin, useMeshNetworkLeave, useMeshNetworkListPeers, useMeshNetworkLogout, useMeshNetworkStartLogin, useMeshNetworkTestConnection, useMetricsProviderCollectSnapshot, useMetricsProviderGetAddonStats, useMetricsProviderGetCached, useMetricsProviderGetCpuTemperature, useMetricsProviderGetCurrent, useMetricsProviderGetDiskSpace, useMetricsProviderGetGpuInfo, useMetricsProviderGetProcessStats, useMetricsProviderKillProcess, useMetricsProviderListAddonInstances, useMetricsProviderListNodeProcesses, useMotionDetectionAnalyze, useMotionDetectionApplyDeviceSettingsPatch, useMotionDetectionGetDeviceLiveContribution, useMotionDetectionGetDeviceSettingsContribution, useMotionDetectionRemoveCamera, useMotionDetectionReset, useMotionGetStatus, useMotionIsDetected, useMotionTriggerGetStatus, useMotionTriggerSetMotionTrigger, useMotionZonesGetOptions, useMotionZonesGetStatus, useMotionZonesSetZone, useMqttBrokerAddBroker, useMqttBrokerGetBrokerConfig, useMqttBrokerGetStatus, useMqttBrokerListBrokers, useMqttBrokerRemoveBroker, useMqttBrokerStartEmbeddedBroker, useMqttBrokerStopEmbeddedBroker, useMqttBrokerTestConnection, useNativeObjectDetectionGetStatus, useNetworkAccessGetEndpoint, useNetworkAccessGetStatus, useNetworkAccessListEndpoints, useNetworkAccessStart, useNetworkAccessStop, useNetworkQualityGetAllStats, useNetworkQualityGetDeviceStats, useNetworkQualityReportClientStats, useNodesClusterAddonStatus, useNodesDeployAddon, useNodesExecuteQuery, useNodesGetCapUsageGraph, useNodesGetNodeAddons, useNodesRenameNode, useNodesRestartAddon, useNodesRestartNode, useNodesRestartProcess, useNodesSetProcessLogLevel, useNodesShutdownNode, useNodesTopology, useNodesUndeployAddon, useNotificationOutputSend, useNotificationOutputSendTest, useOptionalSystem, useOptionalWidgetRegistry, useOsdGetStatus, useOsdSetOverlay, usePTZ, usePipelineAnalyticsApplyDeviceSettingsPatch, usePipelineAnalyticsClearTracks, usePipelineAnalyticsGetActiveTracks, usePipelineAnalyticsGetAudioEvents, usePipelineAnalyticsGetDeviceLiveContribution, usePipelineAnalyticsGetDeviceSettingsContribution, usePipelineAnalyticsGetEventMedia, usePipelineAnalyticsGetMotionEvents, usePipelineAnalyticsGetObjectEvents, usePipelineAnalyticsGetTrack, usePipelineAnalyticsGetTrackMedia, usePipelineAnalyticsListTracks, usePipelineExecutorCacheFrameInPool, usePipelineExecutorDeleteModel, usePipelineExecutorDeleteTemplate, usePipelineExecutorDetect, usePipelineExecutorDownloadModel, usePipelineExecutorGetAddonModels, usePipelineExecutorGetAudioCapabilities, usePipelineExecutorGetAvailableEngines, usePipelineExecutorGetCapabilities, usePipelineExecutorGetDefaultSteps, usePipelineExecutorGetDetectionConfigSchema, usePipelineExecutorGetEffectiveTuning, usePipelineExecutorGetGlobalPipelineConfig, usePipelineExecutorGetGlobalSteps, usePipelineExecutorGetOrchestratorConfigSchema, usePipelineExecutorGetReferenceAudio, usePipelineExecutorGetReferenceAudioFiles, usePipelineExecutorGetReferenceImage, usePipelineExecutorGetSchema, usePipelineExecutorGetSelectedEngine, usePipelineExecutorGetVideoPipelineSteps, usePipelineExecutorInferCached, usePipelineExecutorKillEngine, usePipelineExecutorListLoadedEngines, usePipelineExecutorListReferenceImages, usePipelineExecutorListTemplates, usePipelineExecutorReprobeEngine, usePipelineExecutorRunAudioTest, usePipelineExecutorRunPipeline, usePipelineExecutorRunPipelineBatch, usePipelineExecutorSaveTemplate, usePipelineExecutorSetVideoPipelineSteps, usePipelineExecutorSpinEngine, usePipelineExecutorUncacheFrame, usePipelineExecutorUpdateTemplate, usePipelineOrchestratorApplyDeviceSettingsPatch, usePipelineOrchestratorAssignAudio, usePipelineOrchestratorAssignDecoder, usePipelineOrchestratorAssignPipeline, usePipelineOrchestratorDeleteTemplate, usePipelineOrchestratorGetAgentLoad, usePipelineOrchestratorGetAgentSettings, usePipelineOrchestratorGetAudioAssignment, usePipelineOrchestratorGetAudioAssignments, usePipelineOrchestratorGetAudioNodeLoad, usePipelineOrchestratorGetCameraMetrics, usePipelineOrchestratorGetCameraSettings, usePipelineOrchestratorGetCameraStepOverrides, usePipelineOrchestratorGetCapabilityBindings, usePipelineOrchestratorGetDecoderAssignment, usePipelineOrchestratorGetDecoderAssignments, usePipelineOrchestratorGetDeviceLiveContribution, usePipelineOrchestratorGetDeviceSettingsContribution, usePipelineOrchestratorGetGlobalMetrics, usePipelineOrchestratorGetPipelineAssignment, usePipelineOrchestratorGetPipelineAssignments, usePipelineOrchestratorListAgentSettings, usePipelineOrchestratorListTemplates, usePipelineOrchestratorRebalance, usePipelineOrchestratorRemoveAgentSettings, usePipelineOrchestratorResolvePipeline, usePipelineOrchestratorSaveTemplate, usePipelineOrchestratorSetAgentAddonDefaults, usePipelineOrchestratorSetCameraPipelineForAgent, usePipelineOrchestratorSetCameraStepOverride, usePipelineOrchestratorSetCameraStepToggle, usePipelineOrchestratorSetCapabilityBinding, usePipelineOrchestratorUnassignAudio, usePipelineOrchestratorUnassignDecoder, usePipelineOrchestratorUnassignPipeline, usePipelineOrchestratorUpdateTemplate, usePipelineRunnerAttachCamera, usePipelineRunnerDetachCamera, usePipelineRunnerGetAllCameraMetrics, usePipelineRunnerGetCameraMetrics, usePipelineRunnerGetLocalCameras, usePipelineRunnerGetLocalLoad, usePipelineRunnerGetLocalMetrics, usePipelineRunnerReportMotion, usePlatformProbeGetCapabilities, usePlatformProbeGetHardware, usePlatformProbeGetHardwareEncoders, usePlatformProbeRefreshHardwareEncoders, usePlatformProbeResolveHwAccel, usePlatformProbeResolveInferenceConfig, usePlayerOverlayLayer, usePlayerOverlayLayers, usePlayerToolbarButton, usePlayerToolbarButtons, usePtzAutotrackGetSettings, usePtzAutotrackGetStatus, usePtzAutotrackSetEnabled, usePtzAutotrackSetSettings, usePtzContinuousMove, usePtzDeletePreset, usePtzGetOptions, usePtzGetPosition, usePtzGetPresets, usePtzGetStatus, usePtzGoHome, usePtzGoToPreset, usePtzMove, usePtzSavePreset, usePtzSetAutofocus, usePtzStop, useRebootReboot, useRecordingEngineDisable, useRecordingEngineEnable, useRecordingEngineEstimateGlobalStorage, useRecordingEngineEstimateStorage, useRecordingEngineGetAvailability, useRecordingEngineGetConfig, useRecordingEngineGetMotionStats, useRecordingEngineGetPlaylist, useRecordingEngineGetPolicy, useRecordingEngineGetPolicyStatus, useRecordingEngineGetRetentionConfig, useRecordingEngineGetSegments, useRecordingEngineGetStatus, useRecordingEngineGetStorageUsage, useRecordingEngineGetThumbnail, useRecordingEngineSetPolicy, useRecordingEngineUpdateConfig, useRecordingEngineUpdateRetentionConfig, useRecordingGetPlaybackUrl, useRecordingGetSegments, useRecordingGetThumbnailAt, useRemoteComponent, useSettingsStoreCount, useSettingsStoreDeclareCollection, useSettingsStoreDelete, useSettingsStoreGet, useSettingsStoreInsert, useSettingsStoreIsEmpty, useSettingsStoreQuery, useSettingsStoreSet, useSettingsStoreUpdate, useSnapshotApplyDeviceSettingsPatch, useSnapshotGetDeviceLiveContribution, useSnapshotGetDeviceSettingsContribution, useSnapshotGetSnapshot, useSnapshotGetStatus, useSnapshotInvalidateCache, useSnapshotProviderGetSnapshot, useSnapshotProviderSupportsDevice, useStorageAbortUpload, useStorageBeginDownload, useStorageBeginUpload, useStorageDelete, useStorageDeleteLocation, useStorageEndDownload, useStorageExists, useStorageFinalizeUpload, useStorageGetAvailableSpace, useStorageGetDefaultLocation, useStorageList, useStorageListLocations, useStorageListProviders, useStorageRead, useStorageReadChunk, useStorageResolve, useStorageTestConfig, useStorageTestLocation, useStorageUpsertLocation, useStorageWrite, useStorageWriteChunk, useStreamBrokerApplyDeviceSettingsPatch, useStreamBrokerAssignProfile, useStreamBrokerGetAllRtspEntries, useStreamBrokerGetBrokerStats, useStreamBrokerGetDeviceLiveContribution, useStreamBrokerGetDeviceSettingsContribution, useStreamBrokerGetPreBufferInfo, useStreamBrokerGetRtspEntry, useStreamBrokerGetRtspPort, useStreamBrokerGetStreamUrl, useStreamBrokerGetStreamWithCodec, useStreamBrokerIsRtspEnabled, useStreamBrokerKillClient, useStreamBrokerListAllCameraStreams, useStreamBrokerListAllProfileSlots, useStreamBrokerListClients, useStreamBrokerPublishCameraStream, useStreamBrokerPullAudioChunks, useStreamBrokerPullFrameHandles, useStreamBrokerRegenerateRtspToken, useStreamBrokerReleaseStreamWithCodec, useStreamBrokerRestartProfile, useStreamBrokerRetractCameraStream, useStreamBrokerSetPreBufferDuration, useStreamBrokerSetRtspEnabled, useStreamBrokerSubscribeAudioChunks, useStreamBrokerSubscribeFrames, useStreamBrokerUnassignProfile, useStreamBrokerUnsubscribeAudioChunks, useStreamBrokerUnsubscribeFrames, useStreamParamsGetConfigSchema, useStreamParamsGetOptions, useStreamParamsGetStatus, useStreamParamsSetProfile, useSwitchGetStatus, useSwitchSetState, useSystem, useSystemFeatureFlags, useSystemForceRetentionCleanup, useSystemGetRetentionConfig, useSystemHealth, useSystemInfo, useSystemMutation, useSystemNetworkAddresses, useSystemQuery, useSystemSetRetentionConfig, useThemeMode, useToastOnToast, useTurnProviderGetTurnServers, useUserManagementConfirmTotp, useUserManagementCreateApiKey, useUserManagementCreateScopedToken, useUserManagementCreateUser, useUserManagementDeleteUser, useUserManagementDisableTotp, useUserManagementGetTotpStatus, useUserManagementListApiKeys, useUserManagementListOauthSessions, useUserManagementListScopedTokens, useUserManagementListUsers, useUserManagementOauthExchangeCode, useUserManagementOauthIssueCode, useUserManagementOauthRefresh, useUserManagementOauthVerifyAccessToken, useUserManagementResetPassword, useUserManagementRevokeApiKey, useUserManagementRevokeOauthSession, useUserManagementRevokeScopedToken, useUserManagementSetUserScopes, useUserManagementSetupTotp, useUserManagementUpdateUser, useUserManagementValidateApiKey, useUserManagementValidateCredentials, useUserManagementValidateScopedToken, useUserManagementVerifyTotp, useWebrtcSessionCloseSession, useWebrtcSessionCreateSession, useWebrtcSessionHandleAnswer, useWebrtcSessionHandleOffer, useWebrtcSessionHasAdaptiveBitrate, useWebrtcSessionListStreams, useWidget, useWidgetMetadata, useWidgetRegistry, useZoneAnalyticsGetCameraHistory, useZoneAnalyticsGetCurrentSnapshot, useZoneAnalyticsGetUnzonedHistory, useZoneAnalyticsGetZoneHistory, useZoneEditing, useZoneRulesListRules, useZoneRulesSetRules, useZonesAddZone, useZonesListZones, useZonesRemoveZone, useZonesUpdateZone, validateScopes };
|
|
25853
26810
|
|
|
25854
26811
|
//# sourceMappingURL=index.js.map
|