@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.cjs
CHANGED
|
@@ -37,6 +37,10 @@ let _trpc_client = require("@trpc/client");
|
|
|
37
37
|
_trpc_client = __toESM(_trpc_client, 1);
|
|
38
38
|
let _trpc_react_query = require("@trpc/react-query");
|
|
39
39
|
_trpc_react_query = __toESM(_trpc_react_query, 1);
|
|
40
|
+
let react_konva = require("react-konva");
|
|
41
|
+
react_konva = __toESM(react_konva, 1);
|
|
42
|
+
let konva = require("konva");
|
|
43
|
+
konva = __toESM(konva, 1);
|
|
40
44
|
let _camstack_types = require("@camstack/types");
|
|
41
45
|
let _camstack_sdk = require("@camstack/sdk");
|
|
42
46
|
let zod = require("zod");
|
|
@@ -6875,6 +6879,8 @@ function populateShareCache() {
|
|
|
6875
6879
|
cache.share["@tanstack/react-query"] ??= _tanstack_react_query;
|
|
6876
6880
|
cache.share["@trpc/client"] ??= _trpc_client;
|
|
6877
6881
|
cache.share["@trpc/react-query"] ??= _trpc_react_query;
|
|
6882
|
+
cache.share["react-konva"] ??= react_konva;
|
|
6883
|
+
cache.share["konva"] ??= konva;
|
|
6878
6884
|
cache.share["@camstack/ui-library"] ??= readHostGlobal("__camstackUiLibrary");
|
|
6879
6885
|
cache.share["@camstack/sdk"] ??= readHostGlobal("__camstackSdk");
|
|
6880
6886
|
cache.share["@camstack/types"] ??= readHostGlobal("__camstackTypes");
|
|
@@ -6893,6 +6899,8 @@ function ensureMfHostInit() {
|
|
|
6893
6899
|
"@tanstack/react-query": npmShared(() => _tanstack_react_query),
|
|
6894
6900
|
"@trpc/client": npmShared(() => _trpc_client),
|
|
6895
6901
|
"@trpc/react-query": npmShared(() => _trpc_react_query),
|
|
6902
|
+
"react-konva": npmShared(() => react_konva),
|
|
6903
|
+
"konva": npmShared(() => konva),
|
|
6896
6904
|
"@camstack/ui-library": npmShared(() => readHostGlobal("__camstackUiLibrary")),
|
|
6897
6905
|
"@camstack/sdk": npmShared(() => readHostGlobal("__camstackSdk")),
|
|
6898
6906
|
"@camstack/types": npmShared(() => readHostGlobal("__camstackTypes"))
|
|
@@ -15948,6 +15956,12 @@ var useDecoderPushPacket = trpc.decoder.pushPacket.useQuery;
|
|
|
15948
15956
|
var useDecoderOpenStream = trpc.decoder.openStream.useQuery;
|
|
15949
15957
|
/** Generated alias around `trpc.decoder.pullFrames.useQuery`. */
|
|
15950
15958
|
var useDecoderPullFrames = trpc.decoder.pullFrames.useQuery;
|
|
15959
|
+
/** Generated alias around `trpc.decoder.pullHandles.useQuery`. */
|
|
15960
|
+
var useDecoderPullHandles = trpc.decoder.pullHandles.useQuery;
|
|
15961
|
+
/** Generated alias around `trpc.decoder.getFrame.useQuery`. */
|
|
15962
|
+
var useDecoderGetFrame = trpc.decoder.getFrame.useQuery;
|
|
15963
|
+
/** Generated alias around `trpc.decoder.getShmStats.useQuery`. */
|
|
15964
|
+
var useDecoderGetShmStats = trpc.decoder.getShmStats.useQuery;
|
|
15951
15965
|
/** Generated alias around `trpc.decoder.updateConfig.useQuery`. */
|
|
15952
15966
|
var useDecoderUpdateConfig = trpc.decoder.updateConfig.useQuery;
|
|
15953
15967
|
/** Generated alias around `trpc.decoder.getStats.useQuery`. */
|
|
@@ -16232,6 +16246,12 @@ var useMotionDetectionApplyDeviceSettingsPatch = trpc.motionDetection.applyDevic
|
|
|
16232
16246
|
var useMotionTriggerSetMotionTrigger = trpc.motionTrigger.setMotionTrigger.useMutation;
|
|
16233
16247
|
/** Generated alias around `trpc.motionTrigger.getStatus.useQuery`. */
|
|
16234
16248
|
var useMotionTriggerGetStatus = trpc.motionTrigger.getStatus.useQuery;
|
|
16249
|
+
/** Generated alias around `trpc.motionZones.getOptions.useQuery`. */
|
|
16250
|
+
var useMotionZonesGetOptions = trpc.motionZones.getOptions.useQuery;
|
|
16251
|
+
/** Generated alias around `trpc.motionZones.setZone.useMutation`. */
|
|
16252
|
+
var useMotionZonesSetZone = trpc.motionZones.setZone.useMutation;
|
|
16253
|
+
/** Generated alias around `trpc.motionZones.getStatus.useQuery`. */
|
|
16254
|
+
var useMotionZonesGetStatus = trpc.motionZones.getStatus.useQuery;
|
|
16235
16255
|
/** Generated alias around `trpc.mqttBroker.listBrokers.useQuery`. */
|
|
16236
16256
|
var useMqttBrokerListBrokers = trpc.mqttBroker.listBrokers.useQuery;
|
|
16237
16257
|
/** Generated alias around `trpc.mqttBroker.getBrokerConfig.useQuery`. */
|
|
@@ -16284,6 +16304,8 @@ var useNodesShutdownNode = trpc.nodes.shutdownNode.useMutation;
|
|
|
16284
16304
|
var useNodesRenameNode = trpc.nodes.renameNode.useMutation;
|
|
16285
16305
|
/** Generated alias around `trpc.nodes.clusterAddonStatus.useQuery`. */
|
|
16286
16306
|
var useNodesClusterAddonStatus = trpc.nodes.clusterAddonStatus.useQuery;
|
|
16307
|
+
/** Generated alias around `trpc.nodes.getCapUsageGraph.useQuery`. */
|
|
16308
|
+
var useNodesGetCapUsageGraph = trpc.nodes.getCapUsageGraph.useQuery;
|
|
16287
16309
|
/** Generated alias around `trpc.nodes.getNodeAddons.useQuery`. */
|
|
16288
16310
|
var useNodesGetNodeAddons = trpc.nodes.getNodeAddons.useQuery;
|
|
16289
16311
|
/** Generated alias around `trpc.nodes.setProcessLogLevel.useMutation`. */
|
|
@@ -16502,10 +16524,18 @@ var usePtzStop = trpc.ptz.stop.useMutation;
|
|
|
16502
16524
|
var usePtzGetPresets = trpc.ptz.getPresets.useQuery;
|
|
16503
16525
|
/** Generated alias around `trpc.ptz.goToPreset.useMutation`. */
|
|
16504
16526
|
var usePtzGoToPreset = trpc.ptz.goToPreset.useMutation;
|
|
16527
|
+
/** Generated alias around `trpc.ptz.savePreset.useMutation`. */
|
|
16528
|
+
var usePtzSavePreset = trpc.ptz.savePreset.useMutation;
|
|
16529
|
+
/** Generated alias around `trpc.ptz.deletePreset.useMutation`. */
|
|
16530
|
+
var usePtzDeletePreset = trpc.ptz.deletePreset.useMutation;
|
|
16531
|
+
/** Generated alias around `trpc.ptz.getOptions.useQuery`. */
|
|
16532
|
+
var usePtzGetOptions = trpc.ptz.getOptions.useQuery;
|
|
16505
16533
|
/** Generated alias around `trpc.ptz.goHome.useMutation`. */
|
|
16506
16534
|
var usePtzGoHome = trpc.ptz.goHome.useMutation;
|
|
16507
16535
|
/** Generated alias around `trpc.ptz.getPosition.useQuery`. */
|
|
16508
16536
|
var usePtzGetPosition = trpc.ptz.getPosition.useQuery;
|
|
16537
|
+
/** Generated alias around `trpc.ptz.setAutofocus.useMutation`. */
|
|
16538
|
+
var usePtzSetAutofocus = trpc.ptz.setAutofocus.useMutation;
|
|
16509
16539
|
/** Generated alias around `trpc.ptz.getStatus.useQuery`. */
|
|
16510
16540
|
var usePtzGetStatus = trpc.ptz.getStatus.useQuery;
|
|
16511
16541
|
/** Generated alias around `trpc.ptzAutotrack.getStatus.useQuery`. */
|
|
@@ -16662,8 +16692,18 @@ var useStreamBrokerGetStreamUrl = trpc.streamBroker.getStreamUrl.useQuery;
|
|
|
16662
16692
|
var useStreamBrokerGetStreamWithCodec = trpc.streamBroker.getStreamWithCodec.useMutation;
|
|
16663
16693
|
/** Generated alias around `trpc.streamBroker.releaseStreamWithCodec.useMutation`. */
|
|
16664
16694
|
var useStreamBrokerReleaseStreamWithCodec = trpc.streamBroker.releaseStreamWithCodec.useMutation;
|
|
16665
|
-
/** Generated alias around `trpc.streamBroker.
|
|
16666
|
-
var
|
|
16695
|
+
/** Generated alias around `trpc.streamBroker.subscribeAudioChunks.useMutation`. */
|
|
16696
|
+
var useStreamBrokerSubscribeAudioChunks = trpc.streamBroker.subscribeAudioChunks.useMutation;
|
|
16697
|
+
/** Generated alias around `trpc.streamBroker.pullAudioChunks.useQuery`. */
|
|
16698
|
+
var useStreamBrokerPullAudioChunks = trpc.streamBroker.pullAudioChunks.useQuery;
|
|
16699
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeAudioChunks.useMutation`. */
|
|
16700
|
+
var useStreamBrokerUnsubscribeAudioChunks = trpc.streamBroker.unsubscribeAudioChunks.useMutation;
|
|
16701
|
+
/** Generated alias around `trpc.streamBroker.subscribeFrames.useMutation`. */
|
|
16702
|
+
var useStreamBrokerSubscribeFrames = trpc.streamBroker.subscribeFrames.useMutation;
|
|
16703
|
+
/** Generated alias around `trpc.streamBroker.pullFrameHandles.useQuery`. */
|
|
16704
|
+
var useStreamBrokerPullFrameHandles = trpc.streamBroker.pullFrameHandles.useQuery;
|
|
16705
|
+
/** Generated alias around `trpc.streamBroker.unsubscribeFrames.useMutation`. */
|
|
16706
|
+
var useStreamBrokerUnsubscribeFrames = trpc.streamBroker.unsubscribeFrames.useMutation;
|
|
16667
16707
|
/** Generated alias around `trpc.streamBroker.setPreBufferDuration.useMutation`. */
|
|
16668
16708
|
var useStreamBrokerSetPreBufferDuration = trpc.streamBroker.setPreBufferDuration.useMutation;
|
|
16669
16709
|
/** Generated alias around `trpc.streamBroker.getPreBufferInfo.useQuery`. */
|
|
@@ -16686,6 +16726,14 @@ var useStreamBrokerGetDeviceSettingsContribution = trpc.streamBroker.getDeviceSe
|
|
|
16686
16726
|
var useStreamBrokerGetDeviceLiveContribution = trpc.streamBroker.getDeviceLiveContribution.useQuery;
|
|
16687
16727
|
/** Generated alias around `trpc.streamBroker.applyDeviceSettingsPatch.useMutation`. */
|
|
16688
16728
|
var useStreamBrokerApplyDeviceSettingsPatch = trpc.streamBroker.applyDeviceSettingsPatch.useMutation;
|
|
16729
|
+
/** Generated alias around `trpc.streamParams.getOptions.useQuery`. */
|
|
16730
|
+
var useStreamParamsGetOptions = trpc.streamParams.getOptions.useQuery;
|
|
16731
|
+
/** Generated alias around `trpc.streamParams.setProfile.useMutation`. */
|
|
16732
|
+
var useStreamParamsSetProfile = trpc.streamParams.setProfile.useMutation;
|
|
16733
|
+
/** Generated alias around `trpc.streamParams.getConfigSchema.useQuery`. */
|
|
16734
|
+
var useStreamParamsGetConfigSchema = trpc.streamParams.getConfigSchema.useQuery;
|
|
16735
|
+
/** Generated alias around `trpc.streamParams.getStatus.useQuery`. */
|
|
16736
|
+
var useStreamParamsGetStatus = trpc.streamParams.getStatus.useQuery;
|
|
16689
16737
|
/** Generated alias around `trpc.switch.setState.useMutation`. */
|
|
16690
16738
|
var useSwitchSetState = trpc.switch.setState.useMutation;
|
|
16691
16739
|
/** Generated alias around `trpc.switch.getStatus.useQuery`. */
|
|
@@ -16748,6 +16796,18 @@ var useUserManagementDisableTotp = trpc.userManagement.disableTotp.useMutation;
|
|
|
16748
16796
|
var useUserManagementGetTotpStatus = trpc.userManagement.getTotpStatus.useQuery;
|
|
16749
16797
|
/** Generated alias around `trpc.userManagement.verifyTotp.useMutation`. */
|
|
16750
16798
|
var useUserManagementVerifyTotp = trpc.userManagement.verifyTotp.useMutation;
|
|
16799
|
+
/** Generated alias around `trpc.userManagement.oauthIssueCode.useMutation`. */
|
|
16800
|
+
var useUserManagementOauthIssueCode = trpc.userManagement.oauthIssueCode.useMutation;
|
|
16801
|
+
/** Generated alias around `trpc.userManagement.oauthExchangeCode.useMutation`. */
|
|
16802
|
+
var useUserManagementOauthExchangeCode = trpc.userManagement.oauthExchangeCode.useMutation;
|
|
16803
|
+
/** Generated alias around `trpc.userManagement.oauthRefresh.useMutation`. */
|
|
16804
|
+
var useUserManagementOauthRefresh = trpc.userManagement.oauthRefresh.useMutation;
|
|
16805
|
+
/** Generated alias around `trpc.userManagement.oauthVerifyAccessToken.useQuery`. */
|
|
16806
|
+
var useUserManagementOauthVerifyAccessToken = trpc.userManagement.oauthVerifyAccessToken.useQuery;
|
|
16807
|
+
/** Generated alias around `trpc.userManagement.listOauthSessions.useQuery`. */
|
|
16808
|
+
var useUserManagementListOauthSessions = trpc.userManagement.listOauthSessions.useQuery;
|
|
16809
|
+
/** Generated alias around `trpc.userManagement.revokeOauthSession.useMutation`. */
|
|
16810
|
+
var useUserManagementRevokeOauthSession = trpc.userManagement.revokeOauthSession.useMutation;
|
|
16751
16811
|
/** Generated alias around `trpc.webrtcSession.listStreams.useQuery`. */
|
|
16752
16812
|
var useWebrtcSessionListStreams = trpc.webrtcSession.listStreams.useQuery;
|
|
16753
16813
|
/** Generated alias around `trpc.webrtcSession.createSession.useMutation`. */
|
|
@@ -16791,8 +16851,16 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16791
16851
|
* `vite.widgets.config.ts`). At runtime we fetch the public list of
|
|
16792
16852
|
* addon-contributed widgets via `useAddonWidgetsListWidgets()`, register
|
|
16793
16853
|
* each remote (`registerRemotes`) once, then resolve a widget by
|
|
16794
|
-
* loading the exposed
|
|
16795
|
-
*
|
|
16854
|
+
* loading the exposed module (`loadRemote`) whose default export is a
|
|
16855
|
+
* `Record<componentKey, ComponentType>` map.
|
|
16856
|
+
*
|
|
16857
|
+
* Unified UI-contribution model (Task 10) — a widget declaration IS a
|
|
16858
|
+
* `UiContribution` with `kind:'remote'`. Both the device-detail path
|
|
16859
|
+
* (`ContributionRenderer`) and the dashboard render widgets through the
|
|
16860
|
+
* same `loadRemoteBundle` MF path. `loadRemoteBundle` is exported and
|
|
16861
|
+
* generic over `(remoteName, exposedModule, entryUrl)` so any
|
|
16862
|
+
* `UiContributionRemote` descriptor resolves without an aggregator-keyed
|
|
16863
|
+
* lookup.
|
|
16796
16864
|
*
|
|
16797
16865
|
* Why MF instead of raw ESM:
|
|
16798
16866
|
* - automatic dedup of shared deps (react/react-dom/@tanstack/etc.)
|
|
@@ -16802,9 +16870,7 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16802
16870
|
* wins by default).
|
|
16803
16871
|
* - cross-bundle context invariant preserved automatically: every
|
|
16804
16872
|
* remote's `@camstack/ui-library` import resolves to the host's
|
|
16805
|
-
* instance, so `createContext()` references match across bundles
|
|
16806
|
-
* (the duplicate-Context bug that motivated `createSharedContext`
|
|
16807
|
-
* falls out for free).
|
|
16873
|
+
* instance, so `createContext()` references match across bundles.
|
|
16808
16874
|
*
|
|
16809
16875
|
* Live-update — listens to `addon.widget-ready` so newly-loaded addons
|
|
16810
16876
|
* surface their widgets without a manual page reload (the aggregator
|
|
@@ -16815,11 +16881,11 @@ var useZonesUpdateZone = trpc.zones.updateZone.useMutation;
|
|
|
16815
16881
|
*/
|
|
16816
16882
|
var WidgetRegistryContext = createSharedContext("camstack:widget-registry", null);
|
|
16817
16883
|
/**
|
|
16818
|
-
* Process-global cache for resolved
|
|
16819
|
-
*
|
|
16820
|
-
* default
|
|
16821
|
-
* provider remount — avoids a full re-fetch + re-init of the
|
|
16822
|
-
* when admin-ui's tree cycles.
|
|
16884
|
+
* Process-global cache for resolved remote bundles, keyed by
|
|
16885
|
+
* `<remoteName>::<exposedModule>`. Each cached value is the exposed
|
|
16886
|
+
* module's default-record map (`Record<componentKey, Component>`).
|
|
16887
|
+
* Survives provider remount — avoids a full re-fetch + re-init of the
|
|
16888
|
+
* MF remote when admin-ui's tree cycles.
|
|
16823
16889
|
*/
|
|
16824
16890
|
var bundleModuleCache = /* @__PURE__ */ new Map();
|
|
16825
16891
|
var bundleInflight = /* @__PURE__ */ new Map();
|
|
@@ -16831,6 +16897,10 @@ var bundleInflight = /* @__PURE__ */ new Map();
|
|
|
16831
16897
|
* update.
|
|
16832
16898
|
*/
|
|
16833
16899
|
var registeredRemotes = /* @__PURE__ */ new Set();
|
|
16900
|
+
/** Cache key for `bundleModuleCache` — one entry per `(remote, module)`. */
|
|
16901
|
+
function bundleCacheKey(remoteName, exposedModule) {
|
|
16902
|
+
return `${remoteName}::${exposedModule}`;
|
|
16903
|
+
}
|
|
16834
16904
|
function isRemoteWidgetsModule(value) {
|
|
16835
16905
|
if (!value || typeof value !== "object") return false;
|
|
16836
16906
|
const def = value.default;
|
|
@@ -16839,8 +16909,7 @@ function isRemoteWidgetsModule(value) {
|
|
|
16839
16909
|
/**
|
|
16840
16910
|
* Diagnostic-only string for the "Got: …" tail in the not-a-record
|
|
16841
16911
|
* error. Returns a JSON-friendly snapshot of the module's keys + proto
|
|
16842
|
-
* name without exposing the value itself
|
|
16843
|
-
* thing the module factory produced, may be huge or contain PII).
|
|
16912
|
+
* name without exposing the value itself.
|
|
16844
16913
|
*/
|
|
16845
16914
|
function describeRemoteShape(mod) {
|
|
16846
16915
|
if (!mod || typeof mod !== "object") return typeof mod;
|
|
@@ -16854,13 +16923,19 @@ function describeRemoteShape(mod) {
|
|
|
16854
16923
|
};
|
|
16855
16924
|
}
|
|
16856
16925
|
/**
|
|
16857
|
-
* Register the MF remote (idempotent) and load its
|
|
16858
|
-
* Returns the resolved `Record<
|
|
16926
|
+
* Register the MF remote (idempotent) and load one of its exposed
|
|
16927
|
+
* modules. Returns the resolved `Record<componentKey, Component>` map
|
|
16928
|
+
* (the exposed module's `default` export).
|
|
16929
|
+
*
|
|
16930
|
+
* Exported so the unified `ContributionRenderer` `kind:'remote'` path
|
|
16931
|
+
* can load an arbitrary `{ remoteName, exposedModule }` descriptor —
|
|
16932
|
+
* the same path the `WidgetRegistry` uses internally for widgets.
|
|
16859
16933
|
*/
|
|
16860
|
-
async function loadRemoteBundle(remoteName, entryUrl) {
|
|
16861
|
-
const
|
|
16934
|
+
async function loadRemoteBundle(remoteName, exposedModule, entryUrl) {
|
|
16935
|
+
const cacheKey = bundleCacheKey(remoteName, exposedModule);
|
|
16936
|
+
const cached = bundleModuleCache.get(cacheKey);
|
|
16862
16937
|
if (cached) return cached;
|
|
16863
|
-
const inflight = bundleInflight.get(
|
|
16938
|
+
const inflight = bundleInflight.get(cacheKey);
|
|
16864
16939
|
if (inflight) return inflight;
|
|
16865
16940
|
if (!registeredRemotes.has(remoteName)) {
|
|
16866
16941
|
ensureMfHostInit();
|
|
@@ -16871,22 +16946,26 @@ async function loadRemoteBundle(remoteName, entryUrl) {
|
|
|
16871
16946
|
}], { force: false });
|
|
16872
16947
|
registeredRemotes.add(remoteName);
|
|
16873
16948
|
}
|
|
16874
|
-
const promise = loadRemote(`${remoteName}
|
|
16949
|
+
const promise = loadRemote(`${remoteName}/${exposedModule.startsWith("./") ? exposedModule.slice(2) : exposedModule}`).then((mod) => {
|
|
16875
16950
|
if (!isRemoteWidgetsModule(mod)) {
|
|
16876
16951
|
const shape = describeRemoteShape(mod);
|
|
16877
|
-
throw new Error(`
|
|
16952
|
+
throw new Error(`Remote ${remoteName} (${entryUrl}) does not expose a default record on '${exposedModule}'. Got: ${JSON.stringify(shape)}`);
|
|
16878
16953
|
}
|
|
16879
16954
|
const map = mod.default;
|
|
16880
|
-
bundleModuleCache.set(
|
|
16881
|
-
bundleInflight.delete(
|
|
16955
|
+
bundleModuleCache.set(cacheKey, map);
|
|
16956
|
+
bundleInflight.delete(cacheKey);
|
|
16882
16957
|
return map;
|
|
16883
16958
|
}).catch((err) => {
|
|
16884
|
-
bundleInflight.delete(
|
|
16959
|
+
bundleInflight.delete(cacheKey);
|
|
16885
16960
|
throw err;
|
|
16886
16961
|
});
|
|
16887
|
-
bundleInflight.set(
|
|
16962
|
+
bundleInflight.set(cacheKey, promise);
|
|
16888
16963
|
return promise;
|
|
16889
16964
|
}
|
|
16965
|
+
/** Synchronous peek at the resolved-bundle cache — `undefined` if not yet loaded. */
|
|
16966
|
+
function peekBundle(remoteName, exposedModule) {
|
|
16967
|
+
return bundleModuleCache.get(bundleCacheKey(remoteName, exposedModule));
|
|
16968
|
+
}
|
|
16890
16969
|
var BOOT_WINDOW_MS = 3e4;
|
|
16891
16970
|
var POLL_INTERVAL_MS = 2e3;
|
|
16892
16971
|
function WidgetRegistryProvider({ children }) {
|
|
@@ -16906,21 +16985,24 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16906
16985
|
queryClient.invalidateQueries({ queryKey: [["addonWidgets", "listWidgets"]] });
|
|
16907
16986
|
});
|
|
16908
16987
|
const [resolvedTick, setResolvedTick] = (0, react$1.useState)(0);
|
|
16988
|
+
const widgets = (0, react$1.useMemo)(() => rawWidgets ?? [], [rawWidgets]);
|
|
16909
16989
|
(0, react$1.useEffect)(() => {
|
|
16910
|
-
if (
|
|
16990
|
+
if (widgets.length === 0) return;
|
|
16911
16991
|
let cancelled = false;
|
|
16912
|
-
const
|
|
16913
|
-
for (const w of
|
|
16914
|
-
|
|
16915
|
-
|
|
16916
|
-
|
|
16992
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16993
|
+
for (const w of widgets) {
|
|
16994
|
+
const key = bundleCacheKey(w.remote.remoteName, w.remote.exposedModule);
|
|
16995
|
+
if (seen.has(key)) continue;
|
|
16996
|
+
seen.add(key);
|
|
16997
|
+
if (bundleModuleCache.has(key)) continue;
|
|
16917
16998
|
const entryUrl = w.bundleUrl;
|
|
16918
|
-
loadRemoteBundle(w.remoteName, entryUrl).then(() => {
|
|
16999
|
+
loadRemoteBundle(w.remote.remoteName, w.remote.exposedModule, entryUrl).then(() => {
|
|
16919
17000
|
if (!cancelled) setResolvedTick((t) => t + 1);
|
|
16920
17001
|
}).catch((err) => {
|
|
16921
17002
|
const reason = err instanceof Error ? err.message : String(err);
|
|
16922
17003
|
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load widget remote", {
|
|
16923
|
-
remoteName: w.remoteName,
|
|
17004
|
+
remoteName: w.remote.remoteName,
|
|
17005
|
+
exposedModule: w.remote.exposedModule,
|
|
16924
17006
|
entryUrl,
|
|
16925
17007
|
reason
|
|
16926
17008
|
});
|
|
@@ -16929,19 +17011,26 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16929
17011
|
return () => {
|
|
16930
17012
|
cancelled = true;
|
|
16931
17013
|
};
|
|
16932
|
-
}, [
|
|
17014
|
+
}, [widgets]);
|
|
16933
17015
|
const registry = (0, react$1.useMemo)(() => {
|
|
16934
|
-
const widgets = rawWidgets ?? [];
|
|
16935
17016
|
const byId = /* @__PURE__ */ new Map();
|
|
16936
|
-
|
|
17017
|
+
const entryUrlByRemote = /* @__PURE__ */ new Map();
|
|
17018
|
+
for (const w of widgets) {
|
|
17019
|
+
byId.set(`${w.addonId}/${w.stableId}`, w);
|
|
17020
|
+
entryUrlByRemote.set(w.remote.remoteName, w.bundleUrl);
|
|
17021
|
+
}
|
|
16937
17022
|
const toMetadata = (widgetId, entry) => ({
|
|
16938
17023
|
widgetId,
|
|
16939
17024
|
addonId: entry.addonId,
|
|
16940
17025
|
stableId: entry.stableId,
|
|
17026
|
+
tab: entry.tab,
|
|
17027
|
+
subTab: entry.subTab,
|
|
16941
17028
|
label: entry.label,
|
|
17029
|
+
order: entry.order,
|
|
17030
|
+
kind: entry.kind,
|
|
17031
|
+
remote: entry.remote,
|
|
16942
17032
|
description: entry.description,
|
|
16943
17033
|
icon: entry.icon,
|
|
16944
|
-
remoteName: entry.remoteName,
|
|
16945
17034
|
bundleUrl: entry.bundleUrl,
|
|
16946
17035
|
hosts: entry.hosts,
|
|
16947
17036
|
requires: entry.requires,
|
|
@@ -16954,9 +17043,9 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16954
17043
|
resolve: (widgetId) => {
|
|
16955
17044
|
const entry = byId.get(widgetId);
|
|
16956
17045
|
if (!entry) return void 0;
|
|
16957
|
-
const bundle =
|
|
17046
|
+
const bundle = peekBundle(entry.remote.remoteName, entry.remote.exposedModule);
|
|
16958
17047
|
if (!bundle) return null;
|
|
16959
|
-
const Component = bundle[entry.stableId];
|
|
17048
|
+
const Component = bundle[entry.remote.componentKey ?? entry.stableId];
|
|
16960
17049
|
if (!Component) return void 0;
|
|
16961
17050
|
return Component;
|
|
16962
17051
|
},
|
|
@@ -16969,15 +17058,23 @@ function WidgetRegistryProvider({ children }) {
|
|
|
16969
17058
|
const out = [];
|
|
16970
17059
|
for (const [widgetId, entry] of byId) out.push(toMetadata(widgetId, entry));
|
|
16971
17060
|
return out;
|
|
16972
|
-
}
|
|
17061
|
+
},
|
|
17062
|
+
entryUrlFor: (remoteName) => entryUrlByRemote.get(remoteName)
|
|
16973
17063
|
};
|
|
16974
|
-
}, [
|
|
17064
|
+
}, [widgets, resolvedTick]);
|
|
16975
17065
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetRegistryContext.Provider, {
|
|
16976
17066
|
value: registry,
|
|
16977
17067
|
children
|
|
16978
17068
|
});
|
|
16979
17069
|
}
|
|
16980
|
-
/**
|
|
17070
|
+
/**
|
|
17071
|
+
* Returns the registered widget component, `null` while loading, or
|
|
17072
|
+
* `undefined` if unknown.
|
|
17073
|
+
*
|
|
17074
|
+
* @deprecated Unused by all render paths since the unified
|
|
17075
|
+
* UI-contribution model (Task 10). Use `useRemoteComponent` /
|
|
17076
|
+
* `<WidgetSlot>` instead. Kept for external addons that may import it.
|
|
17077
|
+
*/
|
|
16981
17078
|
function useWidget(widgetId) {
|
|
16982
17079
|
return useWidgetRegistry().resolve(widgetId);
|
|
16983
17080
|
}
|
|
@@ -16991,6 +17088,68 @@ function useWidgetMetadata(widgetId) {
|
|
|
16991
17088
|
function useAllWidgets() {
|
|
16992
17089
|
return useWidgetRegistry().listAll();
|
|
16993
17090
|
}
|
|
17091
|
+
/**
|
|
17092
|
+
* Resolve a `kind:'remote'` `UiContributionRemote` descriptor to a
|
|
17093
|
+
* React component. Drives the unified `ContributionRenderer`
|
|
17094
|
+
* `kind:'remote'` path — both device-detail contributions and dashboard
|
|
17095
|
+
* widgets resolve through the same MF `loadRemoteBundle` loader.
|
|
17096
|
+
*
|
|
17097
|
+
* Returns:
|
|
17098
|
+
* - `undefined` — the remote couldn't be resolved (no entry URL known,
|
|
17099
|
+
* or the remote exposed no component for `componentKey`),
|
|
17100
|
+
* - `null` — the bundle is still loading,
|
|
17101
|
+
* - the component otherwise.
|
|
17102
|
+
*
|
|
17103
|
+
* `entryUrl` may be passed explicitly; when omitted it's resolved from
|
|
17104
|
+
* the registry's aggregator-stamped `bundleUrl` for `remote.remoteName`.
|
|
17105
|
+
*/
|
|
17106
|
+
function useRemoteComponent(remote, entryUrl) {
|
|
17107
|
+
const reg = useOptionalWidgetRegistry();
|
|
17108
|
+
const resolvedEntryUrl = entryUrl ?? reg?.entryUrlFor(remote.remoteName);
|
|
17109
|
+
const [tick, setTick] = (0, react$1.useState)(0);
|
|
17110
|
+
const [loadFailed, setLoadFailed] = (0, react$1.useState)(false);
|
|
17111
|
+
(0, react$1.useEffect)(() => {
|
|
17112
|
+
if (!resolvedEntryUrl) return;
|
|
17113
|
+
setLoadFailed(false);
|
|
17114
|
+
if (peekBundle(remote.remoteName, remote.exposedModule)) return;
|
|
17115
|
+
let cancelled = false;
|
|
17116
|
+
loadRemoteBundle(remote.remoteName, remote.exposedModule, resolvedEntryUrl).then(() => {
|
|
17117
|
+
if (!cancelled) setTick((t) => t + 1);
|
|
17118
|
+
}).catch((err) => {
|
|
17119
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
17120
|
+
(typeof globalThis !== "undefined" ? globalThis.console : void 0)?.error?.("[WidgetRegistry] Failed to load remote component", {
|
|
17121
|
+
remoteName: remote.remoteName,
|
|
17122
|
+
exposedModule: remote.exposedModule,
|
|
17123
|
+
entryUrl: resolvedEntryUrl,
|
|
17124
|
+
reason
|
|
17125
|
+
});
|
|
17126
|
+
if (!cancelled) setLoadFailed(true);
|
|
17127
|
+
});
|
|
17128
|
+
return () => {
|
|
17129
|
+
cancelled = true;
|
|
17130
|
+
};
|
|
17131
|
+
}, [
|
|
17132
|
+
remote.remoteName,
|
|
17133
|
+
remote.exposedModule,
|
|
17134
|
+
resolvedEntryUrl
|
|
17135
|
+
]);
|
|
17136
|
+
return (0, react$1.useMemo)(() => {
|
|
17137
|
+
if (!resolvedEntryUrl) return void 0;
|
|
17138
|
+
if (loadFailed) return void 0;
|
|
17139
|
+
const bundle = peekBundle(remote.remoteName, remote.exposedModule);
|
|
17140
|
+
if (!bundle) return null;
|
|
17141
|
+
const componentKey = remote.componentKey;
|
|
17142
|
+
if (componentKey === void 0) return Object.values(bundle)[0] ?? void 0;
|
|
17143
|
+
return bundle[componentKey] ?? void 0;
|
|
17144
|
+
}, [
|
|
17145
|
+
remote.remoteName,
|
|
17146
|
+
remote.exposedModule,
|
|
17147
|
+
remote.componentKey,
|
|
17148
|
+
resolvedEntryUrl,
|
|
17149
|
+
tick,
|
|
17150
|
+
loadFailed
|
|
17151
|
+
]);
|
|
17152
|
+
}
|
|
16994
17153
|
/** Read the registry instance — throws when no provider is mounted. */
|
|
16995
17154
|
function useWidgetRegistry() {
|
|
16996
17155
|
const ctx = useOptionalWidgetRegistry();
|
|
@@ -17005,163 +17164,1429 @@ function useContextSafe(ctx) {
|
|
|
17005
17164
|
return (0, react$1.useContext)(ctx);
|
|
17006
17165
|
}
|
|
17007
17166
|
//#endregion
|
|
17008
|
-
//#region src/
|
|
17167
|
+
//#region src/hooks/use-ptz.ts
|
|
17009
17168
|
/**
|
|
17010
|
-
*
|
|
17011
|
-
* widget. Consumers reference a widget by its public id
|
|
17012
|
-
* (`<addonId>/<stableId>`), the slot looks the component up in the
|
|
17013
|
-
* shared `WidgetRegistry`, validates host context against the widget's
|
|
17014
|
-
* `requires` metadata, and renders one of:
|
|
17015
|
-
* - skeleton placeholder while the bundle is still loading,
|
|
17016
|
-
* - inline error fallback when the widget id is unknown OR the host
|
|
17017
|
-
* didn't supply a required context (`deviceContext` /
|
|
17018
|
-
* `integrationContext`),
|
|
17019
|
-
* - the resolved component otherwise.
|
|
17169
|
+
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
17020
17170
|
*
|
|
17021
|
-
*
|
|
17022
|
-
*
|
|
17023
|
-
*
|
|
17171
|
+
* Wraps the `ptz` capability methods through the canonical
|
|
17172
|
+
* `useDeviceProxy` surface — every call goes through
|
|
17173
|
+
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
17174
|
+
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
17175
|
+
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
17176
|
+
*
|
|
17177
|
+
* Returns:
|
|
17178
|
+
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
17179
|
+
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
17180
|
+
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
17181
|
+
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
17182
|
+
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
17183
|
+
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
17184
|
+
* - `goHome()`: jump to preset 0.
|
|
17185
|
+
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
17186
|
+
*
|
|
17187
|
+
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
17188
|
+
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
17189
|
+
* addon pages (AddonPageProps.trpc) alike.
|
|
17024
17190
|
*/
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
|
|
17031
|
-
|
|
17032
|
-
|
|
17033
|
-
}
|
|
17034
|
-
|
|
17035
|
-
|
|
17036
|
-
|
|
17037
|
-
}
|
|
17038
|
-
|
|
17039
|
-
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17191
|
+
var DIRECTION_VECTORS = {
|
|
17192
|
+
"up": {
|
|
17193
|
+
pan: 0,
|
|
17194
|
+
tilt: 1
|
|
17195
|
+
},
|
|
17196
|
+
"down": {
|
|
17197
|
+
pan: 0,
|
|
17198
|
+
tilt: -1
|
|
17199
|
+
},
|
|
17200
|
+
"left": {
|
|
17201
|
+
pan: -1,
|
|
17202
|
+
tilt: 0
|
|
17203
|
+
},
|
|
17204
|
+
"right": {
|
|
17205
|
+
pan: 1,
|
|
17206
|
+
tilt: 0
|
|
17207
|
+
},
|
|
17208
|
+
"up-left": {
|
|
17209
|
+
pan: -1,
|
|
17210
|
+
tilt: 1
|
|
17211
|
+
},
|
|
17212
|
+
"up-right": {
|
|
17213
|
+
pan: 1,
|
|
17214
|
+
tilt: 1
|
|
17215
|
+
},
|
|
17216
|
+
"down-left": {
|
|
17217
|
+
pan: -1,
|
|
17218
|
+
tilt: -1
|
|
17219
|
+
},
|
|
17220
|
+
"down-right": {
|
|
17221
|
+
pan: 1,
|
|
17222
|
+
tilt: -1
|
|
17048
17223
|
}
|
|
17049
|
-
|
|
17050
|
-
|
|
17051
|
-
|
|
17052
|
-
|
|
17053
|
-
|
|
17054
|
-
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17073
|
-
|
|
17074
|
-
|
|
17075
|
-
}
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
|
|
17080
|
-
|
|
17081
|
-
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
17082
|
-
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
17083
|
-
const resolvedLabel = resolveLabel(label, translationFn);
|
|
17084
|
-
const resolvedDescription = resolveLabel(description, translationFn);
|
|
17085
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17086
|
-
className: colSpanClass,
|
|
17087
|
-
children: [
|
|
17088
|
-
resolvedLabel !== void 0 && resolvedLabel !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17089
|
-
className: LABEL_CLASS,
|
|
17090
|
-
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17091
|
-
className: "text-danger ml-0.5",
|
|
17092
|
-
children: "*"
|
|
17093
|
-
})]
|
|
17094
|
-
}),
|
|
17095
|
-
children,
|
|
17096
|
-
resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
17097
|
-
className: DESC_CLASS,
|
|
17098
|
-
children: resolvedDescription
|
|
17099
|
-
})
|
|
17100
|
-
]
|
|
17101
|
-
});
|
|
17102
|
-
}
|
|
17103
|
-
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
17104
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
17105
|
-
label: field.label,
|
|
17106
|
-
description: field.description,
|
|
17107
|
-
required: field.required,
|
|
17108
|
-
span: field.span,
|
|
17109
|
-
translationFn,
|
|
17110
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17111
|
-
type: field.inputType ?? "text",
|
|
17112
|
-
className: INPUT_CLASS,
|
|
17113
|
-
value: value === void 0 || value === null ? "" : String(value),
|
|
17114
|
-
placeholder: field.placeholder,
|
|
17115
|
-
maxLength: field.maxLength,
|
|
17116
|
-
pattern: field.pattern,
|
|
17117
|
-
disabled: disabled || field.disabled,
|
|
17118
|
-
onChange: (e) => onChange(e.target.value)
|
|
17119
|
-
})
|
|
17120
|
-
});
|
|
17121
|
-
}
|
|
17122
|
-
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
17123
|
-
const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
|
|
17124
|
-
const focusedRef = (0, react$1.useRef)(false);
|
|
17224
|
+
};
|
|
17225
|
+
function usePTZ(trpc, deviceId, hookOptions) {
|
|
17226
|
+
const defaultSpeed = hookOptions?.defaultSpeed ?? .5;
|
|
17227
|
+
const pulseMs = hookOptions?.pulseMs ?? 250;
|
|
17228
|
+
const enabled = hookOptions?.enabled ?? true;
|
|
17229
|
+
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
17230
|
+
const [presets, setPresets] = (0, react$1.useState)([]);
|
|
17231
|
+
const [options, setOptions] = (0, react$1.useState)(null);
|
|
17232
|
+
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
17233
|
+
const [error, setError] = (0, react$1.useState)(null);
|
|
17234
|
+
const isAbsentProvider = (err) => {
|
|
17235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17236
|
+
return msg.includes("provider not available") || msg.includes("no 'ptz' binding");
|
|
17237
|
+
};
|
|
17238
|
+
const refreshPresets = (0, react$1.useCallback)(async () => {
|
|
17239
|
+
if (!enabled || !ptz) return;
|
|
17240
|
+
try {
|
|
17241
|
+
setPresets(await ptz.getPresets({}));
|
|
17242
|
+
} catch (err) {
|
|
17243
|
+
if (isAbsentProvider(err)) return;
|
|
17244
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17245
|
+
}
|
|
17246
|
+
}, [ptz, enabled]);
|
|
17247
|
+
const refreshOptions = (0, react$1.useCallback)(async () => {
|
|
17248
|
+
if (!enabled || !ptz) return;
|
|
17249
|
+
try {
|
|
17250
|
+
setOptions(await ptz.getOptions({}));
|
|
17251
|
+
} catch (err) {
|
|
17252
|
+
if (isAbsentProvider(err)) return;
|
|
17253
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17254
|
+
}
|
|
17255
|
+
}, [ptz, enabled]);
|
|
17125
17256
|
(0, react$1.useEffect)(() => {
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
}, [
|
|
17129
|
-
const
|
|
17130
|
-
|
|
17131
|
-
|
|
17132
|
-
|
|
17257
|
+
refreshPresets();
|
|
17258
|
+
refreshOptions();
|
|
17259
|
+
}, [refreshPresets, refreshOptions]);
|
|
17260
|
+
const wrap = (0, react$1.useCallback)(async (fn) => {
|
|
17261
|
+
if (!enabled || !ptz) return void 0;
|
|
17262
|
+
setError(null);
|
|
17263
|
+
setBusy(true);
|
|
17264
|
+
try {
|
|
17265
|
+
return await fn();
|
|
17266
|
+
} catch (err) {
|
|
17267
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
17133
17268
|
return;
|
|
17269
|
+
} finally {
|
|
17270
|
+
setBusy(false);
|
|
17134
17271
|
}
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
17142
|
-
|
|
17143
|
-
|
|
17144
|
-
|
|
17145
|
-
|
|
17146
|
-
|
|
17147
|
-
|
|
17148
|
-
|
|
17149
|
-
|
|
17150
|
-
|
|
17151
|
-
|
|
17152
|
-
|
|
17153
|
-
|
|
17154
|
-
|
|
17155
|
-
|
|
17156
|
-
|
|
17157
|
-
|
|
17158
|
-
|
|
17159
|
-
|
|
17160
|
-
|
|
17161
|
-
|
|
17162
|
-
|
|
17163
|
-
|
|
17164
|
-
|
|
17272
|
+
}, [enabled, ptz]);
|
|
17273
|
+
return {
|
|
17274
|
+
move: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17275
|
+
if (!ptz) return;
|
|
17276
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17277
|
+
const s = speed ?? defaultSpeed;
|
|
17278
|
+
await wrap(async () => {
|
|
17279
|
+
await ptz.move({
|
|
17280
|
+
pan: v.pan,
|
|
17281
|
+
tilt: v.tilt,
|
|
17282
|
+
speed: s
|
|
17283
|
+
});
|
|
17284
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17285
|
+
await ptz.stop({});
|
|
17286
|
+
});
|
|
17287
|
+
}, [
|
|
17288
|
+
ptz,
|
|
17289
|
+
defaultSpeed,
|
|
17290
|
+
pulseMs,
|
|
17291
|
+
wrap
|
|
17292
|
+
]),
|
|
17293
|
+
startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17294
|
+
if (!ptz) return;
|
|
17295
|
+
const v = DIRECTION_VECTORS[direction];
|
|
17296
|
+
const s = speed ?? defaultSpeed;
|
|
17297
|
+
await wrap(() => ptz.continuousMove({
|
|
17298
|
+
pan: v.pan,
|
|
17299
|
+
tilt: v.tilt,
|
|
17300
|
+
speed: s
|
|
17301
|
+
}));
|
|
17302
|
+
}, [
|
|
17303
|
+
ptz,
|
|
17304
|
+
defaultSpeed,
|
|
17305
|
+
wrap
|
|
17306
|
+
]),
|
|
17307
|
+
stopContinuous: (0, react$1.useCallback)(async () => {
|
|
17308
|
+
if (!ptz) return;
|
|
17309
|
+
await wrap(() => ptz.stop({}));
|
|
17310
|
+
}, [ptz, wrap]),
|
|
17311
|
+
zoom: (0, react$1.useCallback)(async (direction, speed) => {
|
|
17312
|
+
if (!ptz) return;
|
|
17313
|
+
const z = direction === "in" ? 1 : -1;
|
|
17314
|
+
const s = speed ?? defaultSpeed;
|
|
17315
|
+
await wrap(async () => {
|
|
17316
|
+
await ptz.move({
|
|
17317
|
+
zoom: z,
|
|
17318
|
+
speed: s
|
|
17319
|
+
});
|
|
17320
|
+
await new Promise((r) => setTimeout(r, pulseMs));
|
|
17321
|
+
await ptz.stop({});
|
|
17322
|
+
});
|
|
17323
|
+
}, [
|
|
17324
|
+
ptz,
|
|
17325
|
+
defaultSpeed,
|
|
17326
|
+
pulseMs,
|
|
17327
|
+
wrap
|
|
17328
|
+
]),
|
|
17329
|
+
goHome: (0, react$1.useCallback)(async () => {
|
|
17330
|
+
if (!ptz) return;
|
|
17331
|
+
await wrap(() => ptz.goHome({}));
|
|
17332
|
+
}, [ptz, wrap]),
|
|
17333
|
+
goToPreset: (0, react$1.useCallback)(async (presetId) => {
|
|
17334
|
+
if (!ptz) return;
|
|
17335
|
+
await wrap(() => ptz.goToPreset({ presetId }));
|
|
17336
|
+
}, [ptz, wrap]),
|
|
17337
|
+
savePreset: (0, react$1.useCallback)(async (name) => {
|
|
17338
|
+
if (!ptz) return void 0;
|
|
17339
|
+
return wrap(async () => {
|
|
17340
|
+
const usedIds = new Set(presets.map((p) => Number(p.id)).filter((n) => Number.isInteger(n) && n >= 0));
|
|
17341
|
+
let nextId = 0;
|
|
17342
|
+
while (usedIds.has(nextId)) nextId += 1;
|
|
17343
|
+
const presetId = String(nextId);
|
|
17344
|
+
await ptz.savePreset({
|
|
17345
|
+
presetId,
|
|
17346
|
+
name
|
|
17347
|
+
});
|
|
17348
|
+
await refreshPresets();
|
|
17349
|
+
return presetId;
|
|
17350
|
+
});
|
|
17351
|
+
}, [
|
|
17352
|
+
ptz,
|
|
17353
|
+
presets,
|
|
17354
|
+
refreshPresets,
|
|
17355
|
+
wrap
|
|
17356
|
+
]),
|
|
17357
|
+
deletePreset: (0, react$1.useCallback)(async (presetId) => {
|
|
17358
|
+
if (!ptz) return;
|
|
17359
|
+
await wrap(async () => {
|
|
17360
|
+
await ptz.deletePreset({ presetId });
|
|
17361
|
+
await refreshPresets();
|
|
17362
|
+
});
|
|
17363
|
+
}, [
|
|
17364
|
+
ptz,
|
|
17365
|
+
refreshPresets,
|
|
17366
|
+
wrap
|
|
17367
|
+
]),
|
|
17368
|
+
presets,
|
|
17369
|
+
refreshPresets,
|
|
17370
|
+
options,
|
|
17371
|
+
busy,
|
|
17372
|
+
error
|
|
17373
|
+
};
|
|
17374
|
+
}
|
|
17375
|
+
//#endregion
|
|
17376
|
+
//#region src/composites/ptz-overlay.tsx
|
|
17377
|
+
/**
|
|
17378
|
+
* PTZOverlay — pan / tilt / zoom controls.
|
|
17379
|
+
*
|
|
17380
|
+
* Two visual variants driven by `mode`:
|
|
17381
|
+
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
17382
|
+
* right of the camera viewport. Used as `extraOverlay` on the
|
|
17383
|
+
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
17384
|
+
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
17385
|
+
* legible against any frame.
|
|
17386
|
+
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
17387
|
+
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
17388
|
+
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
17389
|
+
* surrounding `bg-surface` so the dark theme reads consistently
|
|
17390
|
+
* instead of the previous always-dark-bubble look.
|
|
17391
|
+
*
|
|
17392
|
+
* Interaction model is identical across modes: short tap fires a
|
|
17393
|
+
* discrete pulse (`move`); long press starts continuous motion until
|
|
17394
|
+
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
17395
|
+
*/
|
|
17396
|
+
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
17397
|
+
const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
|
|
17398
|
+
const [continuous, setContinuous] = (0, react$1.useState)(false);
|
|
17399
|
+
const handlePointerDown = (0, react$1.useCallback)((e) => {
|
|
17400
|
+
if (disabled) return;
|
|
17401
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
17402
|
+
setPressedAt(Date.now());
|
|
17403
|
+
setContinuous(false);
|
|
17404
|
+
const timer = setTimeout(() => {
|
|
17405
|
+
setContinuous(true);
|
|
17406
|
+
onStart(direction);
|
|
17407
|
+
}, 250);
|
|
17408
|
+
e.currentTarget.dataset.timer = String(timer);
|
|
17409
|
+
}, [
|
|
17410
|
+
direction,
|
|
17411
|
+
disabled,
|
|
17412
|
+
onStart
|
|
17413
|
+
]);
|
|
17414
|
+
const handlePointerUp = (0, react$1.useCallback)((e) => {
|
|
17415
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17416
|
+
if (timerId) clearTimeout(timerId);
|
|
17417
|
+
e.currentTarget.dataset.timer = "";
|
|
17418
|
+
if (continuous) onStop();
|
|
17419
|
+
else if (pressedAt !== null) onMove(direction);
|
|
17420
|
+
setPressedAt(null);
|
|
17421
|
+
setContinuous(false);
|
|
17422
|
+
}, [
|
|
17423
|
+
continuous,
|
|
17424
|
+
direction,
|
|
17425
|
+
onMove,
|
|
17426
|
+
onStop,
|
|
17427
|
+
pressedAt
|
|
17428
|
+
]);
|
|
17429
|
+
const handlePointerCancel = (0, react$1.useCallback)((e) => {
|
|
17430
|
+
const timerId = Number(e.currentTarget.dataset.timer);
|
|
17431
|
+
if (timerId) clearTimeout(timerId);
|
|
17432
|
+
e.currentTarget.dataset.timer = "";
|
|
17433
|
+
if (continuous) onStop();
|
|
17434
|
+
setPressedAt(null);
|
|
17435
|
+
setContinuous(false);
|
|
17436
|
+
}, [continuous, onStop]);
|
|
17437
|
+
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
17438
|
+
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17439
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17440
|
+
type: "button",
|
|
17441
|
+
disabled,
|
|
17442
|
+
onPointerDown: handlePointerDown,
|
|
17443
|
+
onPointerUp: handlePointerUp,
|
|
17444
|
+
onPointerCancel: handlePointerCancel,
|
|
17445
|
+
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),
|
|
17446
|
+
title: direction,
|
|
17447
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
|
|
17448
|
+
});
|
|
17449
|
+
}
|
|
17450
|
+
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
17451
|
+
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, savePreset, deletePreset, presets, options, busy, error } = controls;
|
|
17452
|
+
const confirm = useConfirm();
|
|
17453
|
+
const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
|
|
17454
|
+
const [presetName, setPresetName] = (0, react$1.useState)("");
|
|
17455
|
+
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
17456
|
+
const isPanel = mode === "panel";
|
|
17457
|
+
const presetManagementVisible = isPanel && (options?.supportsPresets ?? false);
|
|
17458
|
+
const maxPresetsReached = options?.maxPresets !== void 0 && presets.length >= options.maxPresets;
|
|
17459
|
+
const handleSavePreset = (0, react$1.useCallback)(async () => {
|
|
17460
|
+
const name = presetName.trim();
|
|
17461
|
+
if (!name || busy || maxPresetsReached) return;
|
|
17462
|
+
if (await savePreset(name) !== void 0) setPresetName("");
|
|
17463
|
+
}, [
|
|
17464
|
+
presetName,
|
|
17465
|
+
busy,
|
|
17466
|
+
maxPresetsReached,
|
|
17467
|
+
savePreset
|
|
17468
|
+
]);
|
|
17469
|
+
const handleDeletePreset = (0, react$1.useCallback)(async (presetId, label) => {
|
|
17470
|
+
if (!await confirm({
|
|
17471
|
+
title: "Delete preset",
|
|
17472
|
+
message: `Delete PTZ preset "${label}"? This removes it from the camera.`,
|
|
17473
|
+
confirmLabel: "Delete",
|
|
17474
|
+
variant: "danger"
|
|
17475
|
+
})) return;
|
|
17476
|
+
await deletePreset(presetId);
|
|
17477
|
+
}, [confirm, deletePreset]);
|
|
17478
|
+
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";
|
|
17479
|
+
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");
|
|
17480
|
+
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
17481
|
+
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
17482
|
+
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
17483
|
+
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
17484
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17485
|
+
className: cn(containerClass, className),
|
|
17486
|
+
children: [
|
|
17487
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17488
|
+
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
17489
|
+
children: ["PTZ: ", error]
|
|
17490
|
+
}),
|
|
17491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17492
|
+
className: cn(rowClass, isPanel && "justify-center"),
|
|
17493
|
+
children: [
|
|
17494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17495
|
+
className: "grid grid-cols-3 gap-0.5",
|
|
17496
|
+
children: [
|
|
17497
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17498
|
+
"aria-hidden": true,
|
|
17499
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17500
|
+
}),
|
|
17501
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17502
|
+
direction: "up",
|
|
17503
|
+
icon: ArrowUp,
|
|
17504
|
+
variant: mode,
|
|
17505
|
+
onMove: move,
|
|
17506
|
+
onStart: startContinuous,
|
|
17507
|
+
onStop: stopContinuous
|
|
17508
|
+
}),
|
|
17509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17510
|
+
"aria-hidden": true,
|
|
17511
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17512
|
+
}),
|
|
17513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17514
|
+
direction: "left",
|
|
17515
|
+
icon: ArrowLeft,
|
|
17516
|
+
variant: mode,
|
|
17517
|
+
onMove: move,
|
|
17518
|
+
onStart: startContinuous,
|
|
17519
|
+
onStop: stopContinuous
|
|
17520
|
+
}),
|
|
17521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17522
|
+
"aria-hidden": true,
|
|
17523
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17524
|
+
}),
|
|
17525
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17526
|
+
direction: "right",
|
|
17527
|
+
icon: ArrowRight,
|
|
17528
|
+
variant: mode,
|
|
17529
|
+
onMove: move,
|
|
17530
|
+
onStart: startContinuous,
|
|
17531
|
+
onStop: stopContinuous
|
|
17532
|
+
}),
|
|
17533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17534
|
+
"aria-hidden": true,
|
|
17535
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17536
|
+
}),
|
|
17537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
17538
|
+
direction: "down",
|
|
17539
|
+
icon: ArrowDown,
|
|
17540
|
+
variant: mode,
|
|
17541
|
+
onMove: move,
|
|
17542
|
+
onStart: startContinuous,
|
|
17543
|
+
onStop: stopContinuous
|
|
17544
|
+
}),
|
|
17545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17546
|
+
"aria-hidden": true,
|
|
17547
|
+
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
17548
|
+
})
|
|
17549
|
+
]
|
|
17550
|
+
}),
|
|
17551
|
+
(showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
|
|
17552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17553
|
+
className: "flex flex-col gap-0.5",
|
|
17554
|
+
children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17555
|
+
type: "button",
|
|
17556
|
+
onClick: () => zoom("in"),
|
|
17557
|
+
disabled: busy,
|
|
17558
|
+
title: "Zoom in",
|
|
17559
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17560
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
|
|
17561
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17562
|
+
type: "button",
|
|
17563
|
+
onClick: () => zoom("out"),
|
|
17564
|
+
disabled: busy,
|
|
17565
|
+
title: "Zoom out",
|
|
17566
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17567
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
|
|
17568
|
+
})] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17569
|
+
type: "button",
|
|
17570
|
+
onClick: () => goHome(),
|
|
17571
|
+
disabled: busy,
|
|
17572
|
+
title: "Go home",
|
|
17573
|
+
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
17574
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
|
|
17575
|
+
})]
|
|
17576
|
+
}),
|
|
17577
|
+
presetsVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17578
|
+
className: "relative",
|
|
17579
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17580
|
+
type: "button",
|
|
17581
|
+
onClick: () => setPresetsOpen((v) => !v),
|
|
17582
|
+
disabled: busy,
|
|
17583
|
+
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"),
|
|
17584
|
+
title: "Presets",
|
|
17585
|
+
children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
17586
|
+
}), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17587
|
+
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"),
|
|
17588
|
+
children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17589
|
+
className: cn("group flex items-center", isPanel ? "hover:bg-surface-hover" : "hover:bg-white/15"),
|
|
17590
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17591
|
+
type: "button",
|
|
17592
|
+
onClick: () => {
|
|
17593
|
+
goToPreset(p.id);
|
|
17594
|
+
setPresetsOpen(false);
|
|
17595
|
+
},
|
|
17596
|
+
disabled: busy,
|
|
17597
|
+
className: cn("flex-1 px-3 py-1.5 text-left text-[10px] disabled:opacity-40", isPanel ? "text-foreground" : "text-white"),
|
|
17598
|
+
children: p.name || p.id
|
|
17599
|
+
}), presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17600
|
+
type: "button",
|
|
17601
|
+
onClick: () => void handleDeletePreset(p.id, p.name || p.id),
|
|
17602
|
+
disabled: busy,
|
|
17603
|
+
title: `Delete preset ${p.name || p.id}`,
|
|
17604
|
+
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"),
|
|
17605
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3 w-3" })
|
|
17606
|
+
})]
|
|
17607
|
+
}, p.id))
|
|
17608
|
+
})]
|
|
17609
|
+
})
|
|
17610
|
+
]
|
|
17611
|
+
}),
|
|
17612
|
+
presetManagementVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17613
|
+
className: "flex flex-col gap-1 rounded-lg border border-border p-2",
|
|
17614
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17615
|
+
className: "flex items-center gap-2",
|
|
17616
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17617
|
+
type: "text",
|
|
17618
|
+
value: presetName,
|
|
17619
|
+
onChange: (e) => setPresetName(e.target.value),
|
|
17620
|
+
onKeyDown: (e) => {
|
|
17621
|
+
if (e.key === "Enter") handleSavePreset();
|
|
17622
|
+
},
|
|
17623
|
+
disabled: busy || maxPresetsReached,
|
|
17624
|
+
placeholder: "New preset name",
|
|
17625
|
+
maxLength: 64,
|
|
17626
|
+
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")
|
|
17627
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17628
|
+
type: "button",
|
|
17629
|
+
onClick: () => void handleSavePreset(),
|
|
17630
|
+
disabled: busy || maxPresetsReached || presetName.trim().length === 0,
|
|
17631
|
+
title: maxPresetsReached ? `Camera preset limit reached (${options?.maxPresets})` : "Save current position as a new preset",
|
|
17632
|
+
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"),
|
|
17633
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), "Save current position"]
|
|
17634
|
+
})]
|
|
17635
|
+
}), maxPresetsReached && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17636
|
+
className: "text-[10px] text-foreground-subtle",
|
|
17637
|
+
children: [
|
|
17638
|
+
"Camera preset limit reached (",
|
|
17639
|
+
options?.maxPresets,
|
|
17640
|
+
"). Delete a preset to add a new one."
|
|
17641
|
+
]
|
|
17642
|
+
})]
|
|
17643
|
+
})
|
|
17644
|
+
]
|
|
17645
|
+
});
|
|
17646
|
+
}
|
|
17647
|
+
//#endregion
|
|
17648
|
+
//#region src/composites/cap-settings/PtzPanel.tsx
|
|
17649
|
+
/**
|
|
17650
|
+
* PtzPanel — live pan/tilt/zoom controls for the `ptz` capability. A
|
|
17651
|
+
* cap-settings component (ui-library), mounted as a cap-contributed
|
|
17652
|
+
* top-level device-detail tab via the cap-UI contribution mechanism
|
|
17653
|
+
* (the `ptz` cap declares `ui: { tab: 'ptz', kind: 'static', ... }`).
|
|
17654
|
+
*
|
|
17655
|
+
* Consolidates the former admin-ui `PTZPanelContent`: `usePTZ` drives
|
|
17656
|
+
* the controls, `PTZOverlay` renders them (`mode='panel'`), and an
|
|
17657
|
+
* Autofocus toggle is shown only when `getOptions().hasAutofocus`.
|
|
17658
|
+
* Autotrack is its own cap-UI contribution (`ptz-autotrack`), mounted
|
|
17659
|
+
* independently by the contribution mechanism — not nested here.
|
|
17660
|
+
*/
|
|
17661
|
+
/** Autofocus toggle — only mounted when `hasAutofocus`. Reads the cap's
|
|
17662
|
+
* `getStatus().autofocus` and drives `setAutofocus`. */
|
|
17663
|
+
function AutofocusToggle({ deviceId }) {
|
|
17664
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
17665
|
+
const [enabled, setEnabled] = (0, react$1.useState)(null);
|
|
17666
|
+
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
17667
|
+
(0, react$1.useEffect)(() => {
|
|
17668
|
+
if (!dev) return void 0;
|
|
17669
|
+
let cancelled = false;
|
|
17670
|
+
(async () => {
|
|
17671
|
+
try {
|
|
17672
|
+
const status = await dev.ptz?.getStatus({});
|
|
17673
|
+
if (cancelled || !status) return;
|
|
17674
|
+
setEnabled(status.autofocus);
|
|
17675
|
+
} catch {}
|
|
17676
|
+
})();
|
|
17677
|
+
return () => {
|
|
17678
|
+
cancelled = true;
|
|
17679
|
+
};
|
|
17680
|
+
}, [dev]);
|
|
17681
|
+
const toggle = (0, react$1.useCallback)(async () => {
|
|
17682
|
+
if (!dev?.ptz || enabled === null) return;
|
|
17683
|
+
setBusy(true);
|
|
17684
|
+
try {
|
|
17685
|
+
const next = !enabled;
|
|
17686
|
+
await dev.ptz.setAutofocus({ enabled: next });
|
|
17687
|
+
setEnabled(next);
|
|
17688
|
+
} catch (err) {
|
|
17689
|
+
console.error("ptz.setAutofocus failed", err);
|
|
17690
|
+
} finally {
|
|
17691
|
+
setBusy(false);
|
|
17692
|
+
}
|
|
17693
|
+
}, [dev, enabled]);
|
|
17694
|
+
if (enabled === null) return null;
|
|
17695
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17696
|
+
className: "flex items-center justify-between border-t border-border/40 mt-2 pt-2",
|
|
17697
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17698
|
+
className: "text-[10.5px] font-semibold text-foreground",
|
|
17699
|
+
children: "Autofocus"
|
|
17700
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17701
|
+
type: "button",
|
|
17702
|
+
onClick: () => void toggle(),
|
|
17703
|
+
disabled: busy,
|
|
17704
|
+
"aria-pressed": enabled,
|
|
17705
|
+
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`,
|
|
17706
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), enabled ? "On" : "Off"]
|
|
17707
|
+
})]
|
|
17708
|
+
});
|
|
17709
|
+
}
|
|
17710
|
+
function PtzPanel({ deviceId }) {
|
|
17711
|
+
const system = useSystem();
|
|
17712
|
+
const ready = Number.isFinite(deviceId);
|
|
17713
|
+
const controls = usePTZ(system.trpcClient, ready ? deviceId : 0, { enabled: ready });
|
|
17714
|
+
if (!ready) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17715
|
+
className: "flex items-center justify-center h-full text-[11px] text-foreground-subtle italic",
|
|
17716
|
+
children: [
|
|
17717
|
+
"Device #",
|
|
17718
|
+
deviceId,
|
|
17719
|
+
" not loaded."
|
|
17720
|
+
]
|
|
17721
|
+
});
|
|
17722
|
+
const hasAutofocus = controls.options?.hasAutofocus === true;
|
|
17723
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17724
|
+
className: "flex flex-col h-full overflow-y-auto",
|
|
17725
|
+
children: [
|
|
17726
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
17727
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider mb-2",
|
|
17728
|
+
children: "PTZ"
|
|
17729
|
+
}),
|
|
17730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PTZOverlay, {
|
|
17731
|
+
controls,
|
|
17732
|
+
mode: "panel"
|
|
17733
|
+
}),
|
|
17734
|
+
hasAutofocus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutofocusToggle, { deviceId })
|
|
17735
|
+
]
|
|
17736
|
+
});
|
|
17737
|
+
}
|
|
17738
|
+
//#endregion
|
|
17739
|
+
//#region src/hooks/use-device-autotrack.ts
|
|
17740
|
+
/**
|
|
17741
|
+
* `useDeviceAutotrack` — typed wrapper around the `ptz-autotrack` cap.
|
|
17742
|
+
*
|
|
17743
|
+
* Surface:
|
|
17744
|
+
* - `status` — current `{enabled, lastChangedAt, currentSettings}`,
|
|
17745
|
+
* polled at 5s intervals while the hook is mounted.
|
|
17746
|
+
* - `setEnabled(on)` — flip the on/off state.
|
|
17747
|
+
* - `setSettings(partial)` — patch one or more settings keys.
|
|
17748
|
+
* - `isPending` — true while a mutation is in flight.
|
|
17749
|
+
*
|
|
17750
|
+
* Returns `null` for `status` when the cap isn't available (device
|
|
17751
|
+
* doesn't support autotrack) — callers gate render on this OR on the
|
|
17752
|
+
* device's `PtzAutotrack` feature flag.
|
|
17753
|
+
*/
|
|
17754
|
+
function useDeviceAutotrack(deviceId) {
|
|
17755
|
+
const queryClient = (0, _tanstack_react_query.useQueryClient)();
|
|
17756
|
+
const statusQuery = usePtzAutotrackGetStatus({ deviceId: deviceId ?? 0 }, {
|
|
17757
|
+
enabled: deviceId !== null && Number.isFinite(deviceId),
|
|
17758
|
+
refetchInterval: 5e3,
|
|
17759
|
+
retry: 1
|
|
17760
|
+
});
|
|
17761
|
+
const setEnabledMutation = usePtzAutotrackSetEnabled({ onSuccess: () => {
|
|
17762
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17763
|
+
} });
|
|
17764
|
+
const setSettingsMutation = usePtzAutotrackSetSettings({ onSuccess: () => {
|
|
17765
|
+
queryClient.invalidateQueries({ queryKey: [["ptzAutotrack"]] });
|
|
17766
|
+
} });
|
|
17767
|
+
const setEnabled = (0, react$1.useCallback)(async (on) => {
|
|
17768
|
+
if (deviceId === null) return;
|
|
17769
|
+
await setEnabledMutation.mutateAsync({
|
|
17770
|
+
deviceId,
|
|
17771
|
+
enabled: on
|
|
17772
|
+
});
|
|
17773
|
+
}, [deviceId, setEnabledMutation]);
|
|
17774
|
+
const setSettings = (0, react$1.useCallback)(async (patch) => {
|
|
17775
|
+
if (deviceId === null) return;
|
|
17776
|
+
await setSettingsMutation.mutateAsync({
|
|
17777
|
+
deviceId,
|
|
17778
|
+
settings: patch
|
|
17779
|
+
});
|
|
17780
|
+
}, [deviceId, setSettingsMutation]);
|
|
17781
|
+
const errorMsg = (() => {
|
|
17782
|
+
if (statusQuery.error) return statusQuery.error.message;
|
|
17783
|
+
if (setEnabledMutation.error) return setEnabledMutation.error.message;
|
|
17784
|
+
if (setSettingsMutation.error) return setSettingsMutation.error.message;
|
|
17785
|
+
return null;
|
|
17786
|
+
})();
|
|
17787
|
+
return {
|
|
17788
|
+
status: statusQuery.data ?? null,
|
|
17789
|
+
isLoading: statusQuery.isLoading,
|
|
17790
|
+
isPending: setEnabledMutation.isPending || setSettingsMutation.isPending,
|
|
17791
|
+
error: errorMsg,
|
|
17792
|
+
setEnabled,
|
|
17793
|
+
setSettings
|
|
17794
|
+
};
|
|
17795
|
+
}
|
|
17796
|
+
//#endregion
|
|
17797
|
+
//#region src/composites/cap-settings/AutotrackSection.tsx
|
|
17798
|
+
/**
|
|
17799
|
+
* Autotrack settings card — rendered inside the PTZ panel when the
|
|
17800
|
+
* device has `DeviceFeature.PtzAutotrack`. Three knobs (cross-vendor
|
|
17801
|
+
* schema): target type, stop delay, disappear delay, plus an enable /
|
|
17802
|
+
* disable toggle that drives the `ptz-autotrack` cap directly.
|
|
17803
|
+
*
|
|
17804
|
+
* Save semantics:
|
|
17805
|
+
* - Toggle (enable/disable) calls `setEnabled` immediately.
|
|
17806
|
+
* - Form fields debounce-save on blur — no explicit Save button.
|
|
17807
|
+
*
|
|
17808
|
+
* Vendor capability hints:
|
|
17809
|
+
* - The cap's status carries `currentSettings` from the camera's
|
|
17810
|
+
* own GET; rendered on every poll so the operator sees what's
|
|
17811
|
+
* really applied vs what they typed.
|
|
17812
|
+
* - When the firmware ignored a setting, the field stays editable.
|
|
17813
|
+
*/
|
|
17814
|
+
function AutotrackSection({ deviceId }) {
|
|
17815
|
+
const { status, isLoading, isPending, error, setEnabled, setSettings } = useDeviceAutotrack(Number.isFinite(deviceId) ? deviceId : null);
|
|
17816
|
+
const [draft, setDraft] = (0, react$1.useState)({
|
|
17817
|
+
targetType: "",
|
|
17818
|
+
stopDelaySeconds: 30,
|
|
17819
|
+
disappearDelaySeconds: 15
|
|
17820
|
+
});
|
|
17821
|
+
const [editing, setEditing] = (0, react$1.useState)(false);
|
|
17822
|
+
(0, react$1.useEffect)(() => {
|
|
17823
|
+
if (editing) return;
|
|
17824
|
+
const s = status?.currentSettings;
|
|
17825
|
+
if (!s) return;
|
|
17826
|
+
setDraft({
|
|
17827
|
+
targetType: s.targetType,
|
|
17828
|
+
stopDelaySeconds: s.stopDelaySeconds,
|
|
17829
|
+
disappearDelaySeconds: s.disappearDelaySeconds
|
|
17830
|
+
});
|
|
17831
|
+
}, [status, editing]);
|
|
17832
|
+
if (isLoading && !status) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17833
|
+
className: "flex items-center gap-2 px-3 py-2 text-[10.5px] text-foreground-subtle",
|
|
17834
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3 w-3 animate-spin" }), "Loading autotrack…"]
|
|
17835
|
+
});
|
|
17836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17837
|
+
className: "border-t border-border/40 mt-2 pt-2 px-1 space-y-2",
|
|
17838
|
+
children: [
|
|
17839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17840
|
+
className: "flex items-center justify-between px-2",
|
|
17841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
|
|
17842
|
+
className: "flex items-center gap-1.5 text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
17843
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Crosshair, { className: "h-3 w-3 text-primary" }), "Autotrack"]
|
|
17844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
17845
|
+
type: "button",
|
|
17846
|
+
onClick: () => {
|
|
17847
|
+
setEnabled(!(status?.enabled ?? false));
|
|
17848
|
+
},
|
|
17849
|
+
disabled: isPending,
|
|
17850
|
+
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`,
|
|
17851
|
+
"aria-pressed": status?.enabled ?? false,
|
|
17852
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `h-1.5 w-1.5 rounded-full ${status?.enabled ? "bg-success" : "bg-foreground-subtle/40"}` }), status?.enabled ? "Enabled" : "Disabled"]
|
|
17853
|
+
})]
|
|
17854
|
+
}),
|
|
17855
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17856
|
+
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",
|
|
17857
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CircleAlert, { className: "h-3 w-3 flex-shrink-0 mt-0.5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17858
|
+
className: "leading-snug break-words",
|
|
17859
|
+
children: error
|
|
17860
|
+
})]
|
|
17861
|
+
}),
|
|
17862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17863
|
+
className: "grid grid-cols-2 gap-2 px-2",
|
|
17864
|
+
children: [
|
|
17865
|
+
(status?.supportedTargetTypes?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17866
|
+
className: "col-span-2 flex flex-col gap-1",
|
|
17867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17868
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17869
|
+
children: "Target type"
|
|
17870
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
17871
|
+
value: draft.targetType,
|
|
17872
|
+
onFocus: () => setEditing(true),
|
|
17873
|
+
onChange: (e) => setDraft((d) => ({
|
|
17874
|
+
...d,
|
|
17875
|
+
targetType: e.target.value
|
|
17876
|
+
})),
|
|
17877
|
+
onBlur: () => {
|
|
17878
|
+
setEditing(false);
|
|
17879
|
+
const current = status?.currentSettings?.targetType ?? "";
|
|
17880
|
+
if (draft.targetType !== current) setSettings({ targetType: draft.targetType });
|
|
17881
|
+
},
|
|
17882
|
+
disabled: isPending,
|
|
17883
|
+
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",
|
|
17884
|
+
children: (status?.supportedTargetTypes ?? []).map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17885
|
+
value: opt.value,
|
|
17886
|
+
children: opt.label
|
|
17887
|
+
}, opt.value))
|
|
17888
|
+
})]
|
|
17889
|
+
}),
|
|
17890
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17891
|
+
className: "flex flex-col gap-1",
|
|
17892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17893
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17894
|
+
children: "Stop delay (s)"
|
|
17895
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17896
|
+
type: "number",
|
|
17897
|
+
min: 0,
|
|
17898
|
+
max: 300,
|
|
17899
|
+
value: draft.stopDelaySeconds,
|
|
17900
|
+
onFocus: () => setEditing(true),
|
|
17901
|
+
onChange: (e) => setDraft((d) => ({
|
|
17902
|
+
...d,
|
|
17903
|
+
stopDelaySeconds: Number(e.target.value) || 0
|
|
17904
|
+
})),
|
|
17905
|
+
onBlur: () => {
|
|
17906
|
+
setEditing(false);
|
|
17907
|
+
const current = status?.currentSettings?.stopDelaySeconds ?? 30;
|
|
17908
|
+
if (draft.stopDelaySeconds !== current) setSettings({ stopDelaySeconds: draft.stopDelaySeconds });
|
|
17909
|
+
},
|
|
17910
|
+
disabled: isPending,
|
|
17911
|
+
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"
|
|
17912
|
+
})]
|
|
17913
|
+
}),
|
|
17914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
17915
|
+
className: "flex flex-col gap-1",
|
|
17916
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17917
|
+
className: "text-[10px] font-medium text-foreground-subtle",
|
|
17918
|
+
children: "Disappear delay (s)"
|
|
17919
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
17920
|
+
type: "number",
|
|
17921
|
+
min: 0,
|
|
17922
|
+
max: 300,
|
|
17923
|
+
value: draft.disappearDelaySeconds,
|
|
17924
|
+
onFocus: () => setEditing(true),
|
|
17925
|
+
onChange: (e) => setDraft((d) => ({
|
|
17926
|
+
...d,
|
|
17927
|
+
disappearDelaySeconds: Number(e.target.value) || 0
|
|
17928
|
+
})),
|
|
17929
|
+
onBlur: () => {
|
|
17930
|
+
setEditing(false);
|
|
17931
|
+
const current = status?.currentSettings?.disappearDelaySeconds ?? 15;
|
|
17932
|
+
if (draft.disappearDelaySeconds !== current) setSettings({ disappearDelaySeconds: draft.disappearDelaySeconds });
|
|
17933
|
+
},
|
|
17934
|
+
disabled: isPending,
|
|
17935
|
+
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"
|
|
17936
|
+
})]
|
|
17937
|
+
})
|
|
17938
|
+
]
|
|
17939
|
+
}),
|
|
17940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
17941
|
+
className: "px-2 text-[9.5px] text-foreground-subtle leading-snug italic",
|
|
17942
|
+
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)."
|
|
17943
|
+
})
|
|
17944
|
+
]
|
|
17945
|
+
});
|
|
17946
|
+
}
|
|
17947
|
+
//#endregion
|
|
17948
|
+
//#region src/contexts/player-overlays.tsx
|
|
17949
|
+
/**
|
|
17950
|
+
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
17951
|
+
* the device-detail live-frame `StreamPanel`.
|
|
17952
|
+
*
|
|
17953
|
+
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
17954
|
+
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
17955
|
+
* editing state by hand. Adding a new feature (audio waveform,
|
|
17956
|
+
* detection bbox tracker, recording controls, …) meant another
|
|
17957
|
+
* round of plumbing through the host file and StreamPanel's prop
|
|
17958
|
+
* surface. The registry lets a sibling component (Detection tab,
|
|
17959
|
+
* Live Stats tab, an addon-page extension) declare its own layer
|
|
17960
|
+
* imperatively via a hook, and the host renders whatever's
|
|
17961
|
+
* registered.
|
|
17962
|
+
*
|
|
17963
|
+
* Two registries live side-by-side:
|
|
17964
|
+
*
|
|
17965
|
+
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
17966
|
+
* video frame (zones polygon canvas, motion bbox overlay,
|
|
17967
|
+
* audio waveform). Rendered ordered by `order` (lower first
|
|
17968
|
+
* → bottom; higher → top); the last-registered layer wins
|
|
17969
|
+
* ties.
|
|
17970
|
+
*
|
|
17971
|
+
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
17972
|
+
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
17973
|
+
* Each carries an icon, label, controlled `active` flag, and
|
|
17974
|
+
* a click handler. The host (StreamPanel) renders them
|
|
17975
|
+
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
17976
|
+
*
|
|
17977
|
+
* Lifecycle: hooks register their layer / button on mount and
|
|
17978
|
+
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
17979
|
+
* automatically tears down the registration. Re-registering with
|
|
17980
|
+
* the same `id` replaces the prior entry — meaning a single hook
|
|
17981
|
+
* call can update its overlay/button props on every render without
|
|
17982
|
+
* leaking entries.
|
|
17983
|
+
*
|
|
17984
|
+
* Provider scope: typically wraps the whole device-detail subtree
|
|
17985
|
+
* so the StreamPanel + every tab share the same registry. One
|
|
17986
|
+
* provider per `deviceId`; switching device IDs unmounts the
|
|
17987
|
+
* provider naturally.
|
|
17988
|
+
*/
|
|
17989
|
+
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
17990
|
+
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
17991
|
+
function PlayerOverlaysProvider({ children }) {
|
|
17992
|
+
const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
17993
|
+
const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
17994
|
+
const setLayer = (0, react$1.useCallback)((layer) => {
|
|
17995
|
+
setLayers((prev) => {
|
|
17996
|
+
const next = new Map(prev);
|
|
17997
|
+
next.set(layer.id, layer);
|
|
17998
|
+
return next;
|
|
17999
|
+
});
|
|
18000
|
+
}, []);
|
|
18001
|
+
const removeLayer = (0, react$1.useCallback)((id) => {
|
|
18002
|
+
setLayers((prev) => {
|
|
18003
|
+
if (!prev.has(id)) return prev;
|
|
18004
|
+
const next = new Map(prev);
|
|
18005
|
+
next.delete(id);
|
|
18006
|
+
return next;
|
|
18007
|
+
});
|
|
18008
|
+
}, []);
|
|
18009
|
+
const setButton = (0, react$1.useCallback)((button) => {
|
|
18010
|
+
setButtons((prev) => {
|
|
18011
|
+
const next = new Map(prev);
|
|
18012
|
+
next.set(button.id, button);
|
|
18013
|
+
return next;
|
|
18014
|
+
});
|
|
18015
|
+
}, []);
|
|
18016
|
+
const removeButton = (0, react$1.useCallback)((id) => {
|
|
18017
|
+
setButtons((prev) => {
|
|
18018
|
+
if (!prev.has(id)) return prev;
|
|
18019
|
+
const next = new Map(prev);
|
|
18020
|
+
next.delete(id);
|
|
18021
|
+
return next;
|
|
18022
|
+
});
|
|
18023
|
+
}, []);
|
|
18024
|
+
const stateValue = (0, react$1.useMemo)(() => ({
|
|
18025
|
+
layers,
|
|
18026
|
+
buttons
|
|
18027
|
+
}), [layers, buttons]);
|
|
18028
|
+
const actionsValue = (0, react$1.useMemo)(() => ({
|
|
18029
|
+
setLayer,
|
|
18030
|
+
removeLayer,
|
|
18031
|
+
setButton,
|
|
18032
|
+
removeButton
|
|
18033
|
+
}), [
|
|
18034
|
+
setLayer,
|
|
18035
|
+
removeLayer,
|
|
18036
|
+
setButton,
|
|
18037
|
+
removeButton
|
|
18038
|
+
]);
|
|
18039
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
|
|
18040
|
+
value: stateValue,
|
|
18041
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
|
|
18042
|
+
value: actionsValue,
|
|
18043
|
+
children
|
|
18044
|
+
})
|
|
18045
|
+
});
|
|
18046
|
+
}
|
|
18047
|
+
/** Snapshot of registered layers ordered by `order` (asc, ties resolved
|
|
18048
|
+
* by insertion order of the underlying Map). Returns `[]` outside a
|
|
18049
|
+
* provider. */
|
|
18050
|
+
function usePlayerOverlayLayers() {
|
|
18051
|
+
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
18052
|
+
return (0, react$1.useMemo)(() => {
|
|
18053
|
+
if (!state) return [];
|
|
18054
|
+
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
18055
|
+
}, [state]);
|
|
18056
|
+
}
|
|
18057
|
+
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
18058
|
+
function usePlayerToolbarButtons() {
|
|
18059
|
+
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
18060
|
+
return (0, react$1.useMemo)(() => {
|
|
18061
|
+
if (!state) return [];
|
|
18062
|
+
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
18063
|
+
}, [state]);
|
|
18064
|
+
}
|
|
18065
|
+
/**
|
|
18066
|
+
* Register an overlay layer for the lifetime of the calling component.
|
|
18067
|
+
* Re-registers on every render with the latest spec; auto-unregisters
|
|
18068
|
+
* on unmount. Pass `null` to skip registration when the layer is
|
|
18069
|
+
* conditionally enabled (the hook still runs every render — keeps
|
|
18070
|
+
* react-hook order stable across spec === null toggles).
|
|
18071
|
+
*
|
|
18072
|
+
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
18073
|
+
* to avoid re-registering on every parent render — context-write
|
|
18074
|
+
* effects depend on referential equality of the spec object.
|
|
18075
|
+
*/
|
|
18076
|
+
function usePlayerOverlayLayer(spec) {
|
|
18077
|
+
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
18078
|
+
(0, react$1.useEffect)(() => {
|
|
18079
|
+
if (!actions || !spec) return void 0;
|
|
18080
|
+
actions.setLayer(spec);
|
|
18081
|
+
return () => actions.removeLayer(spec.id);
|
|
18082
|
+
}, [actions, spec]);
|
|
18083
|
+
}
|
|
18084
|
+
/** Same shape as `usePlayerOverlayLayer`, scoped to toolbar buttons. */
|
|
18085
|
+
function usePlayerToolbarButton(spec) {
|
|
18086
|
+
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
18087
|
+
(0, react$1.useEffect)(() => {
|
|
18088
|
+
if (!actions || !spec) return void 0;
|
|
18089
|
+
actions.setButton(spec);
|
|
18090
|
+
return () => actions.removeButton(spec.id);
|
|
18091
|
+
}, [actions, spec]);
|
|
18092
|
+
}
|
|
18093
|
+
//#endregion
|
|
18094
|
+
//#region src/composites/cap-settings/MotionGridCanvas.tsx
|
|
18095
|
+
/**
|
|
18096
|
+
* MotionGridCanvas — the on-frame motion-zone grid editor.
|
|
18097
|
+
*
|
|
18098
|
+
* This component is *only* the editable lattice painted over the live
|
|
18099
|
+
* frame: a plain CSS-grid of `gridWidth × gridHeight` cells. It carries
|
|
18100
|
+
* NO controls — enabled / sensitivity / save / quick-actions all live
|
|
18101
|
+
* in the management bar inside the settings section (`MotionZonesTab`),
|
|
18102
|
+
* so the live frame stays unobstructed.
|
|
18103
|
+
*
|
|
18104
|
+
* Interaction:
|
|
18105
|
+
* - Click a cell → toggle it.
|
|
18106
|
+
* - Press + drag → paint every touched cell to whatever the
|
|
18107
|
+
* first-touched cell flipped TO (the standard grid-mask gesture).
|
|
18108
|
+
*
|
|
18109
|
+
* Purely controlled — every change is forwarded through `onCellsChange`;
|
|
18110
|
+
* the owning `MotionZonesTab` keeps the single source of truth.
|
|
18111
|
+
*/
|
|
18112
|
+
function MotionGridCanvas({ options, cells, onCellsChange }) {
|
|
18113
|
+
const { gridWidth, gridHeight } = options;
|
|
18114
|
+
const total = gridWidth * gridHeight;
|
|
18115
|
+
const paintingRef = (0, react$1.useRef)(false);
|
|
18116
|
+
const paintValueRef = (0, react$1.useRef)(true);
|
|
18117
|
+
const paintedRef = (0, react$1.useRef)(/* @__PURE__ */ new Set());
|
|
18118
|
+
const [, forceTick] = (0, react$1.useState)(0);
|
|
18119
|
+
const applyCell = (0, react$1.useCallback)((index, value) => {
|
|
18120
|
+
if (index < 0 || index >= total) return;
|
|
18121
|
+
if (cells[index] === value) return;
|
|
18122
|
+
const next = [...cells];
|
|
18123
|
+
while (next.length < total) next.push(false);
|
|
18124
|
+
next.length = total;
|
|
18125
|
+
next[index] = value;
|
|
18126
|
+
onCellsChange(next);
|
|
18127
|
+
}, [
|
|
18128
|
+
cells,
|
|
18129
|
+
total,
|
|
18130
|
+
onCellsChange
|
|
18131
|
+
]);
|
|
18132
|
+
const handlePointerDown = (0, react$1.useCallback)((index) => (e) => {
|
|
18133
|
+
e.preventDefault();
|
|
18134
|
+
const nextValue = !cells[index];
|
|
18135
|
+
paintingRef.current = true;
|
|
18136
|
+
paintValueRef.current = nextValue;
|
|
18137
|
+
paintedRef.current = new Set([index]);
|
|
18138
|
+
applyCell(index, nextValue);
|
|
18139
|
+
forceTick((t) => t + 1);
|
|
18140
|
+
}, [cells, applyCell]);
|
|
18141
|
+
const handlePointerEnter = (0, react$1.useCallback)((index) => () => {
|
|
18142
|
+
if (!paintingRef.current) return;
|
|
18143
|
+
if (paintedRef.current.has(index)) return;
|
|
18144
|
+
paintedRef.current.add(index);
|
|
18145
|
+
applyCell(index, paintValueRef.current);
|
|
18146
|
+
}, [applyCell]);
|
|
18147
|
+
const endPaint = (0, react$1.useCallback)(() => {
|
|
18148
|
+
if (!paintingRef.current) return;
|
|
18149
|
+
paintingRef.current = false;
|
|
18150
|
+
paintedRef.current = /* @__PURE__ */ new Set();
|
|
18151
|
+
}, []);
|
|
18152
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18153
|
+
className: "absolute inset-0 select-none grid",
|
|
18154
|
+
style: {
|
|
18155
|
+
gridTemplateColumns: `repeat(${String(gridWidth)}, 1fr)`,
|
|
18156
|
+
gridTemplateRows: `repeat(${String(gridHeight)}, 1fr)`
|
|
18157
|
+
},
|
|
18158
|
+
onPointerUp: endPaint,
|
|
18159
|
+
onPointerLeave: endPaint,
|
|
18160
|
+
children: Array.from({ length: total }, (_, i) => {
|
|
18161
|
+
const active = cells[i] === true;
|
|
18162
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18163
|
+
type: "button",
|
|
18164
|
+
"aria-label": `motion cell ${String(i)}`,
|
|
18165
|
+
"aria-pressed": active,
|
|
18166
|
+
onPointerDown: handlePointerDown(i),
|
|
18167
|
+
onPointerEnter: handlePointerEnter(i),
|
|
18168
|
+
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"
|
|
18169
|
+
}, i);
|
|
18170
|
+
})
|
|
18171
|
+
});
|
|
18172
|
+
}
|
|
18173
|
+
//#endregion
|
|
18174
|
+
//#region src/composites/cap-settings/MotionZonesSettings.tsx
|
|
18175
|
+
/**
|
|
18176
|
+
* MotionZonesSettings — management surface for the on-camera
|
|
18177
|
+
* `motion-zones` capability. A cap-settings component (ui-library),
|
|
18178
|
+
* mounted into the device-detail Config tab via the cap-UI
|
|
18179
|
+
* contribution mechanism (the `motion-zones` cap declares a `ui` block).
|
|
18180
|
+
*
|
|
18181
|
+
* Split of concerns (mirrors the analytics zones drawer):
|
|
18182
|
+
* - The editable GRID is painted over the live frame by
|
|
18183
|
+
* `MotionGridCanvas`, registered as a player-overlay layer only
|
|
18184
|
+
* while the operator has toggled "Edit grid" on (OFF by default).
|
|
18185
|
+
* - Every CONTROL lives here, in the settings section, NOT on the
|
|
18186
|
+
* frame: a quick-action bar (All on / All off / Invert), Save /
|
|
18187
|
+
* Revert, and the cell-count status.
|
|
18188
|
+
* - Detection enable + sensitivity are NOT edited here — they are
|
|
18189
|
+
* the camera's on-board motion master switch, owned by the
|
|
18190
|
+
* driver's "On-camera motion" settings section. The grid editor
|
|
18191
|
+
* only paints the mask (`setZone` with a `cells`-only patch;
|
|
18192
|
+
* enable / sensitivity are left untouched camera-side).
|
|
18193
|
+
*
|
|
18194
|
+
* The grid editor draws nothing on cameras without the `motion-zones`
|
|
18195
|
+
* cap — the fetch self-gates on a failed `getOptions`.
|
|
18196
|
+
*/
|
|
18197
|
+
/** "Camera doesn't expose the cap" — swallow so the editor stays
|
|
18198
|
+
* hidden on unsupported devices (mirrors `usePTZ`'s gate). */
|
|
18199
|
+
function isAbsentProvider(err) {
|
|
18200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
18201
|
+
return msg.includes("provider not available") || msg.includes("motion-zones");
|
|
18202
|
+
}
|
|
18203
|
+
/** Coerce an arbitrary `cells` array to exactly `gridWidth*gridHeight`. */
|
|
18204
|
+
function normaliseCells(cells, options) {
|
|
18205
|
+
const total = options.gridWidth * options.gridHeight;
|
|
18206
|
+
const next = new Array(total);
|
|
18207
|
+
for (let i = 0; i < total; i += 1) next[i] = cells[i] === true;
|
|
18208
|
+
return next;
|
|
18209
|
+
}
|
|
18210
|
+
function cellsEqual(a, b) {
|
|
18211
|
+
if (a.length !== b.length) return false;
|
|
18212
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
18213
|
+
return true;
|
|
18214
|
+
}
|
|
18215
|
+
function MotionZonesSettings({ deviceId }) {
|
|
18216
|
+
const dev = useDeviceProxy(useSystem().trpcClient, deviceId);
|
|
18217
|
+
const [options, setOptions] = (0, react$1.useState)(null);
|
|
18218
|
+
const [unsupported, setUnsupported] = (0, react$1.useState)(false);
|
|
18219
|
+
const [cells, setCells] = (0, react$1.useState)(null);
|
|
18220
|
+
const [committed, setCommitted] = (0, react$1.useState)(null);
|
|
18221
|
+
const [saving, setSaving] = (0, react$1.useState)(false);
|
|
18222
|
+
const [editingGrid, setEditingGrid] = (0, react$1.useState)(false);
|
|
18223
|
+
const seededRef = (0, react$1.useRef)(false);
|
|
18224
|
+
(0, react$1.useEffect)(() => {
|
|
18225
|
+
if (!dev) return void 0;
|
|
18226
|
+
let cancelled = false;
|
|
18227
|
+
(async () => {
|
|
18228
|
+
try {
|
|
18229
|
+
const opts = await dev.motionZones?.getOptions({});
|
|
18230
|
+
if (cancelled || !opts) return;
|
|
18231
|
+
setOptions(opts);
|
|
18232
|
+
if (seededRef.current) return;
|
|
18233
|
+
const status = await dev.motionZones?.getStatus({});
|
|
18234
|
+
if (cancelled || !status) return;
|
|
18235
|
+
seededRef.current = true;
|
|
18236
|
+
const norm = normaliseCells(status.cells, opts);
|
|
18237
|
+
setCommitted(norm);
|
|
18238
|
+
setCells(norm);
|
|
18239
|
+
} catch (err) {
|
|
18240
|
+
if (cancelled) return;
|
|
18241
|
+
if (isAbsentProvider(err)) setUnsupported(true);
|
|
18242
|
+
else console.error("motion-zones load failed", err);
|
|
18243
|
+
}
|
|
18244
|
+
})();
|
|
18245
|
+
return () => {
|
|
18246
|
+
cancelled = true;
|
|
18247
|
+
};
|
|
18248
|
+
}, [dev]);
|
|
18249
|
+
const dirty = (0, react$1.useMemo)(() => cells !== null && committed !== null && !cellsEqual(cells, committed), [cells, committed]);
|
|
18250
|
+
const activeCount = (0, react$1.useMemo)(() => cells ? cells.reduce((n, c) => c ? n + 1 : n, 0) : 0, [cells]);
|
|
18251
|
+
const total = options ? options.gridWidth * options.gridHeight : 0;
|
|
18252
|
+
const setAll = (0, react$1.useCallback)((value) => {
|
|
18253
|
+
if (total > 0) setCells(new Array(total).fill(value));
|
|
18254
|
+
}, [total]);
|
|
18255
|
+
const invert = (0, react$1.useCallback)(() => {
|
|
18256
|
+
setCells((prev) => prev ? prev.map((c) => !c) : prev);
|
|
18257
|
+
}, []);
|
|
18258
|
+
const revert = (0, react$1.useCallback)(() => {
|
|
18259
|
+
if (committed) setCells([...committed]);
|
|
18260
|
+
}, [committed]);
|
|
18261
|
+
const save = (0, react$1.useCallback)(async () => {
|
|
18262
|
+
if (!cells || !dev?.motionZones || !options) return;
|
|
18263
|
+
setSaving(true);
|
|
18264
|
+
try {
|
|
18265
|
+
await dev.motionZones.setZone({ patch: { cells: [...cells] } });
|
|
18266
|
+
const fresh = await dev.motionZones.getStatus({});
|
|
18267
|
+
if (fresh) {
|
|
18268
|
+
const norm = normaliseCells(fresh.cells, options);
|
|
18269
|
+
setCommitted(norm);
|
|
18270
|
+
setCells(norm);
|
|
18271
|
+
}
|
|
18272
|
+
} catch (err) {
|
|
18273
|
+
console.error("motion-zones.setZone failed", err);
|
|
18274
|
+
} finally {
|
|
18275
|
+
setSaving(false);
|
|
18276
|
+
}
|
|
18277
|
+
}, [
|
|
18278
|
+
cells,
|
|
18279
|
+
dev,
|
|
18280
|
+
options
|
|
18281
|
+
]);
|
|
18282
|
+
usePlayerOverlayLayer((0, react$1.useMemo)(() => editingGrid && !unsupported && options && cells ? {
|
|
18283
|
+
id: "motion-zones",
|
|
18284
|
+
order: 110,
|
|
18285
|
+
node: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MotionGridCanvas, {
|
|
18286
|
+
options,
|
|
18287
|
+
cells,
|
|
18288
|
+
onCellsChange: setCells
|
|
18289
|
+
})
|
|
18290
|
+
} : null, [
|
|
18291
|
+
editingGrid,
|
|
18292
|
+
unsupported,
|
|
18293
|
+
options,
|
|
18294
|
+
cells
|
|
18295
|
+
]));
|
|
18296
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18297
|
+
className: "flex flex-col gap-3",
|
|
18298
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
18299
|
+
className: "text-[11px] font-semibold text-foreground-subtle uppercase tracking-wider",
|
|
18300
|
+
children: "Motion Zones"
|
|
18301
|
+
}), unsupported ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18302
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18303
|
+
children: "This camera doesn't expose an on-board motion-detection grid."
|
|
18304
|
+
}) : !(!unsupported && options !== null && cells !== null) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18305
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18306
|
+
children: "Loading the camera's motion grid…"
|
|
18307
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
18308
|
+
className: `${TEXT_HINT} leading-relaxed`,
|
|
18309
|
+
children: [
|
|
18310
|
+
"Toggle",
|
|
18311
|
+
" ",
|
|
18312
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18313
|
+
className: "text-foreground",
|
|
18314
|
+
children: "Edit grid"
|
|
18315
|
+
}),
|
|
18316
|
+
" to paint the region the camera watches for motion directly on the live frame, then ",
|
|
18317
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18318
|
+
className: "text-foreground",
|
|
18319
|
+
children: "Save"
|
|
18320
|
+
}),
|
|
18321
|
+
" to push the mask to the camera. Detection on/off and sensitivity are set in the",
|
|
18322
|
+
" ",
|
|
18323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
18324
|
+
className: "text-foreground",
|
|
18325
|
+
children: "On-camera motion"
|
|
18326
|
+
}),
|
|
18327
|
+
" section."
|
|
18328
|
+
]
|
|
18329
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18330
|
+
className: "flex items-center gap-2 flex-wrap",
|
|
18331
|
+
children: [
|
|
18332
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18333
|
+
type: "button",
|
|
18334
|
+
onClick: () => setEditingGrid((v) => !v),
|
|
18335
|
+
disabled: saving,
|
|
18336
|
+
"aria-pressed": editingGrid,
|
|
18337
|
+
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",
|
|
18338
|
+
children: editingGrid ? "Done editing" : "Edit grid"
|
|
18339
|
+
}),
|
|
18340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18341
|
+
type: "button",
|
|
18342
|
+
onClick: () => setAll(true),
|
|
18343
|
+
disabled: saving,
|
|
18344
|
+
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",
|
|
18345
|
+
children: "All on"
|
|
18346
|
+
}),
|
|
18347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18348
|
+
type: "button",
|
|
18349
|
+
onClick: () => setAll(false),
|
|
18350
|
+
disabled: saving,
|
|
18351
|
+
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",
|
|
18352
|
+
children: "All off"
|
|
18353
|
+
}),
|
|
18354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18355
|
+
type: "button",
|
|
18356
|
+
onClick: invert,
|
|
18357
|
+
disabled: saving,
|
|
18358
|
+
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",
|
|
18359
|
+
children: "Invert"
|
|
18360
|
+
}),
|
|
18361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
18362
|
+
className: `${TEXT_HINT} ml-1 tabular-nums`,
|
|
18363
|
+
children: [
|
|
18364
|
+
activeCount,
|
|
18365
|
+
" / ",
|
|
18366
|
+
total,
|
|
18367
|
+
" cells · ",
|
|
18368
|
+
options.gridWidth,
|
|
18369
|
+
"×",
|
|
18370
|
+
options.gridHeight
|
|
18371
|
+
]
|
|
18372
|
+
}),
|
|
18373
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "flex-1" }),
|
|
18374
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18375
|
+
type: "button",
|
|
18376
|
+
onClick: revert,
|
|
18377
|
+
disabled: saving || !dirty,
|
|
18378
|
+
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",
|
|
18379
|
+
children: "Revert"
|
|
18380
|
+
}),
|
|
18381
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
18382
|
+
type: "button",
|
|
18383
|
+
onClick: () => void save(),
|
|
18384
|
+
disabled: saving || !dirty,
|
|
18385
|
+
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",
|
|
18386
|
+
children: saving ? "Saving…" : "Save"
|
|
18387
|
+
})
|
|
18388
|
+
]
|
|
18389
|
+
})] })]
|
|
18390
|
+
});
|
|
18391
|
+
}
|
|
18392
|
+
//#endregion
|
|
18393
|
+
//#region src/widgets/host-widgets.ts
|
|
18394
|
+
/** Host widgets — ui-library React components embeddable as a
|
|
18395
|
+
* `type:'widget'` ConfigField. `WidgetSlot` resolves these BEFORE the
|
|
18396
|
+
* Module-Federation `WidgetRegistry`. ids are namespaced `host/<name>`. */
|
|
18397
|
+
var HOST_WIDGETS = {
|
|
18398
|
+
"host/motion-zones-grid": MotionZonesSettings,
|
|
18399
|
+
"host/ptz-panel": PtzPanel,
|
|
18400
|
+
"host/ptz-autotrack": AutotrackSection
|
|
18401
|
+
};
|
|
18402
|
+
//#endregion
|
|
18403
|
+
//#region src/composites/widget-slot.tsx
|
|
18404
|
+
/**
|
|
18405
|
+
* <WidgetSlot> — single host-side mount point for any addon-contributed
|
|
18406
|
+
* widget. Consumers reference a widget by its public id
|
|
18407
|
+
* (`<addonId>/<stableId>`), the slot looks the widget's metadata up in
|
|
18408
|
+
* the shared `WidgetRegistry`, resolves the component through the
|
|
18409
|
+
* unified `useRemoteComponent` Module-Federation loader (the SAME path
|
|
18410
|
+
* `ContributionRenderer`'s `kind:'remote'` branch uses), validates host
|
|
18411
|
+
* context against the widget's `requires` metadata, and renders one of:
|
|
18412
|
+
* - skeleton placeholder while the bundle is still loading,
|
|
18413
|
+
* - inline error fallback when the widget id is unknown OR the host
|
|
18414
|
+
* didn't supply a required context (`deviceContext` /
|
|
18415
|
+
* `integrationContext`),
|
|
18416
|
+
* - the resolved component otherwise.
|
|
18417
|
+
*
|
|
18418
|
+
* The slot is intentionally STUPID — no styling beyond the skeleton/
|
|
18419
|
+
* error fallback. Layout (card vs inline, sizing) is the host's
|
|
18420
|
+
* responsibility.
|
|
18421
|
+
*/
|
|
18422
|
+
function WidgetSlot(props) {
|
|
18423
|
+
const { widgetId, deviceId } = props;
|
|
18424
|
+
const HostComponent = HOST_WIDGETS[widgetId];
|
|
18425
|
+
if (HostComponent !== void 0) {
|
|
18426
|
+
if (deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18427
|
+
widgetId,
|
|
18428
|
+
reason: "missing-device-context"
|
|
18429
|
+
});
|
|
18430
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostComponent, { deviceId });
|
|
18431
|
+
}
|
|
18432
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemoteWidgetSlot, { ...props });
|
|
18433
|
+
}
|
|
18434
|
+
/**
|
|
18435
|
+
* Module-Federation remote-widget path — looks the widget's metadata up
|
|
18436
|
+
* in the shared `WidgetRegistry` and resolves it through `useRemoteComponent`.
|
|
18437
|
+
*/
|
|
18438
|
+
function RemoteWidgetSlot(props) {
|
|
18439
|
+
const { widgetId } = props;
|
|
18440
|
+
const metadata = useWidgetMetadata(widgetId);
|
|
18441
|
+
if (metadata === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18442
|
+
widgetId,
|
|
18443
|
+
reason: "unknown"
|
|
18444
|
+
});
|
|
18445
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResolvedWidgetSlot, {
|
|
18446
|
+
...props,
|
|
18447
|
+
metadata
|
|
18448
|
+
});
|
|
18449
|
+
}
|
|
18450
|
+
/**
|
|
18451
|
+
* Inner host — resolves the widget component through the unified
|
|
18452
|
+
* `useRemoteComponent` MF loader (shared with `ContributionRenderer`'s
|
|
18453
|
+
* `kind:'remote'` branch). Mounted only once a `metadata` descriptor is
|
|
18454
|
+
* known, so the hook always receives a valid `remote` descriptor.
|
|
18455
|
+
*/
|
|
18456
|
+
function ResolvedWidgetSlot(props) {
|
|
18457
|
+
const { widgetId, host = "device-tab", config, deviceId, integrationId, instanceId, size, columns, rows, metadata } = props;
|
|
18458
|
+
const Component = useRemoteComponent(metadata.remote, metadata.bundleUrl);
|
|
18459
|
+
const resolvedInstanceId = (0, react$1.useMemo)(() => instanceId ?? widgetId, [instanceId, widgetId]);
|
|
18460
|
+
if (Component === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSkeleton, {});
|
|
18461
|
+
if (Component === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18462
|
+
widgetId,
|
|
18463
|
+
reason: "missing-export"
|
|
18464
|
+
});
|
|
18465
|
+
if (metadata.requires.deviceContext && deviceId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18466
|
+
widgetId,
|
|
18467
|
+
reason: "missing-device-context"
|
|
18468
|
+
});
|
|
18469
|
+
if (metadata.requires.integrationContext && integrationId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMissingError, {
|
|
18470
|
+
widgetId,
|
|
18471
|
+
reason: "missing-integration-context"
|
|
18472
|
+
});
|
|
18473
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
|
|
18474
|
+
instanceId: resolvedInstanceId,
|
|
18475
|
+
host,
|
|
18476
|
+
config,
|
|
18477
|
+
deviceId,
|
|
18478
|
+
integrationId,
|
|
18479
|
+
size,
|
|
18480
|
+
columns,
|
|
18481
|
+
rows
|
|
18482
|
+
});
|
|
18483
|
+
}
|
|
18484
|
+
function WidgetSkeleton() {
|
|
18485
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18486
|
+
className: "rounded-lg border border-border bg-surface/40 p-4 animate-pulse",
|
|
18487
|
+
children: [
|
|
18488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-3 w-24 bg-foreground-subtle/20 rounded mb-2" }),
|
|
18489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-full bg-foreground-subtle/10 rounded mb-1" }),
|
|
18490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 w-3/4 bg-foreground-subtle/10 rounded" })
|
|
18491
|
+
]
|
|
18492
|
+
});
|
|
18493
|
+
}
|
|
18494
|
+
function WidgetMissingError({ widgetId, reason }) {
|
|
18495
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
18496
|
+
className: "rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-xs text-warning",
|
|
18497
|
+
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`
|
|
18498
|
+
});
|
|
18499
|
+
}
|
|
18500
|
+
//#endregion
|
|
18501
|
+
//#region src/composites/config-form-field.tsx
|
|
18502
|
+
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";
|
|
18503
|
+
var LABEL_CLASS = "block text-[11px] font-medium text-foreground mb-1";
|
|
18504
|
+
var DESC_CLASS = "text-[10px] text-foreground-subtle mt-0.5";
|
|
18505
|
+
function FieldWrapper({ label, description, required, span, children, translationFn }) {
|
|
18506
|
+
const colSpanClass = span === 2 ? "col-span-2" : span === 3 ? "col-span-3" : span === 4 ? "col-span-4" : "col-span-1";
|
|
18507
|
+
const resolvedLabel = resolveLabel(label, translationFn);
|
|
18508
|
+
const resolvedDescription = resolveLabel(description, translationFn);
|
|
18509
|
+
const hasLabel = resolvedLabel !== void 0 && resolvedLabel !== "";
|
|
18510
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18511
|
+
className: `${colSpanClass} min-w-0`,
|
|
18512
|
+
children: [
|
|
18513
|
+
hasLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
18514
|
+
className: LABEL_CLASS,
|
|
18515
|
+
children: [resolvedLabel, required && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18516
|
+
className: "text-danger ml-0.5",
|
|
18517
|
+
children: "*"
|
|
18518
|
+
})]
|
|
18519
|
+
}),
|
|
18520
|
+
children,
|
|
18521
|
+
resolvedDescription && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
18522
|
+
className: DESC_CLASS,
|
|
18523
|
+
children: resolvedDescription
|
|
18524
|
+
})
|
|
18525
|
+
]
|
|
18526
|
+
});
|
|
18527
|
+
}
|
|
18528
|
+
function TextField({ field, value, onChange, disabled, translationFn }) {
|
|
18529
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
18530
|
+
label: field.label,
|
|
18531
|
+
description: field.description,
|
|
18532
|
+
required: field.required,
|
|
18533
|
+
span: field.span,
|
|
18534
|
+
translationFn,
|
|
18535
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18536
|
+
type: field.inputType ?? "text",
|
|
18537
|
+
className: INPUT_CLASS,
|
|
18538
|
+
value: value === void 0 || value === null ? "" : String(value),
|
|
18539
|
+
placeholder: field.placeholder,
|
|
18540
|
+
maxLength: field.maxLength,
|
|
18541
|
+
pattern: field.pattern,
|
|
18542
|
+
disabled: disabled || field.disabled,
|
|
18543
|
+
onChange: (e) => onChange(e.target.value)
|
|
18544
|
+
})
|
|
18545
|
+
});
|
|
18546
|
+
}
|
|
18547
|
+
function NumberField({ field, value, onChange, disabled, translationFn }) {
|
|
18548
|
+
const [local, setLocal] = (0, react$1.useState)(value === void 0 || value === null ? "" : String(value));
|
|
18549
|
+
const focusedRef = (0, react$1.useRef)(false);
|
|
18550
|
+
(0, react$1.useEffect)(() => {
|
|
18551
|
+
if (focusedRef.current) return;
|
|
18552
|
+
setLocal(value === void 0 || value === null ? "" : String(value));
|
|
18553
|
+
}, [value]);
|
|
18554
|
+
const handleChange = (raw) => {
|
|
18555
|
+
setLocal(raw);
|
|
18556
|
+
if (raw === "" || raw === "-") {
|
|
18557
|
+
onChange(void 0);
|
|
18558
|
+
return;
|
|
18559
|
+
}
|
|
18560
|
+
const parsed = Number(raw);
|
|
18561
|
+
if (Number.isNaN(parsed)) return;
|
|
18562
|
+
onChange(parsed);
|
|
18563
|
+
};
|
|
18564
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldWrapper, {
|
|
18565
|
+
label: field.label,
|
|
18566
|
+
description: field.description,
|
|
18567
|
+
required: field.required,
|
|
18568
|
+
span: field.span,
|
|
18569
|
+
translationFn,
|
|
18570
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
18571
|
+
className: "flex items-center gap-1",
|
|
18572
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
18573
|
+
type: "number",
|
|
18574
|
+
className: INPUT_CLASS,
|
|
18575
|
+
value: local,
|
|
18576
|
+
placeholder: field.placeholder,
|
|
18577
|
+
min: field.min,
|
|
18578
|
+
max: field.max,
|
|
18579
|
+
step: field.step,
|
|
18580
|
+
disabled: disabled || field.disabled,
|
|
18581
|
+
onFocus: () => {
|
|
18582
|
+
focusedRef.current = true;
|
|
18583
|
+
},
|
|
18584
|
+
onBlur: () => {
|
|
18585
|
+
focusedRef.current = false;
|
|
18586
|
+
},
|
|
18587
|
+
onChange: (e) => handleChange(e.target.value)
|
|
18588
|
+
}), field.unit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
18589
|
+
className: "text-xs text-foreground-subtle whitespace-nowrap",
|
|
17165
18590
|
children: field.unit
|
|
17166
18591
|
})]
|
|
17167
18592
|
})
|
|
@@ -18270,18 +19695,12 @@ function AddonActionButtonField({ field, values, disabled, onAction }) {
|
|
|
18270
19695
|
}
|
|
18271
19696
|
function WidgetField({ field }) {
|
|
18272
19697
|
const deviceId = useDeviceId();
|
|
18273
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
18274
|
-
|
|
18275
|
-
|
|
18276
|
-
|
|
18277
|
-
|
|
18278
|
-
|
|
18279
|
-
widgetId: field.widgetId,
|
|
18280
|
-
host: "device-tab",
|
|
18281
|
-
config: field.widgetConfig,
|
|
18282
|
-
deviceId: deviceId ?? void 0,
|
|
18283
|
-
instanceId: field.key
|
|
18284
|
-
})
|
|
19698
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSlot, {
|
|
19699
|
+
widgetId: field.widgetId,
|
|
19700
|
+
host: "device-tab",
|
|
19701
|
+
config: field.widgetConfig,
|
|
19702
|
+
deviceId: deviceId ?? void 0,
|
|
19703
|
+
instanceId: field.key
|
|
18285
19704
|
});
|
|
18286
19705
|
}
|
|
18287
19706
|
function formatReadonlyValue(value, unit) {
|
|
@@ -18772,7 +20191,7 @@ function ConfigFormBuilder({ schema, values, onChange, disabled, translationFn,
|
|
|
18772
20191
|
* + patch loop. `onAfterChange` fires once the patch is applied so
|
|
18773
20192
|
* consumers can re-read any derived state.
|
|
18774
20193
|
*/
|
|
18775
|
-
var Chevron = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
20194
|
+
var Chevron$1 = ({ open }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
18776
20195
|
className: `h-3 w-3 transition-transform ${open ? "rotate-90" : ""}`,
|
|
18777
20196
|
viewBox: "0 0 24 24",
|
|
18778
20197
|
fill: "none",
|
|
@@ -18937,7 +20356,7 @@ function AddonGlobalSettingsForm({ trpc, addonId, nodeId, title, disabled, onAft
|
|
|
18937
20356
|
className: "w-full px-4 py-2 flex items-center gap-2 hover:bg-surface-hover text-left",
|
|
18938
20357
|
"aria-expanded": open,
|
|
18939
20358
|
children: [
|
|
18940
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { open }),
|
|
20359
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron$1, { open }),
|
|
18941
20360
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
18942
20361
|
className: "text-xs font-semibold text-foreground uppercase tracking-wide flex-1",
|
|
18943
20362
|
children: title ?? addonId
|
|
@@ -19507,184 +20926,38 @@ function CameraStreamPlayer({ serverUrl, streamKey, label, autoPlay = true, mute
|
|
|
19507
20926
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
19508
20927
|
className: "h-3.5 w-3.5",
|
|
19509
20928
|
viewBox: "0 0 24 24",
|
|
19510
|
-
fill: "none",
|
|
19511
|
-
stroke: "currentColor",
|
|
19512
|
-
strokeWidth: "2",
|
|
19513
|
-
strokeLinecap: "round",
|
|
19514
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
|
|
19515
|
-
})
|
|
19516
|
-
})]
|
|
19517
|
-
})]
|
|
19518
|
-
})
|
|
19519
|
-
]
|
|
19520
|
-
});
|
|
19521
|
-
}
|
|
19522
|
-
function ToolbarButton$1({ onClick, title, children }) {
|
|
19523
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
19524
|
-
onClick,
|
|
19525
|
-
title,
|
|
19526
|
-
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
19527
|
-
children
|
|
19528
|
-
});
|
|
19529
|
-
}
|
|
19530
|
-
async function waitForIceGathering(pc) {
|
|
19531
|
-
if (pc.iceGatheringState === "complete") return;
|
|
19532
|
-
return new Promise((resolve) => {
|
|
19533
|
-
const handler = () => {
|
|
19534
|
-
if (pc.iceGatheringState === "complete") {
|
|
19535
|
-
pc.removeEventListener("icegatheringstatechange", handler);
|
|
19536
|
-
resolve();
|
|
19537
|
-
}
|
|
19538
|
-
};
|
|
19539
|
-
pc.addEventListener("icegatheringstatechange", handler);
|
|
19540
|
-
setTimeout(resolve, 5e3);
|
|
19541
|
-
});
|
|
19542
|
-
}
|
|
19543
|
-
//#endregion
|
|
19544
|
-
//#region src/contexts/player-overlays.tsx
|
|
19545
|
-
/**
|
|
19546
|
-
* Player overlay registry — pluggable layers + toolbar buttons for
|
|
19547
|
-
* the device-detail live-frame `StreamPanel`.
|
|
19548
|
-
*
|
|
19549
|
-
* Why a registry: previously `StreamPanelContent` hardcoded each
|
|
19550
|
-
* overlay (PTZ, intercom toggle, zone editor) and threaded the
|
|
19551
|
-
* editing state by hand. Adding a new feature (audio waveform,
|
|
19552
|
-
* detection bbox tracker, recording controls, …) meant another
|
|
19553
|
-
* round of plumbing through the host file and StreamPanel's prop
|
|
19554
|
-
* surface. The registry lets a sibling component (Detection tab,
|
|
19555
|
-
* Live Stats tab, an addon-page extension) declare its own layer
|
|
19556
|
-
* imperatively via a hook, and the host renders whatever's
|
|
19557
|
-
* registered.
|
|
19558
|
-
*
|
|
19559
|
-
* Two registries live side-by-side:
|
|
19560
|
-
*
|
|
19561
|
-
* 1. **layers** — absolute-positioned React nodes drawn over the
|
|
19562
|
-
* video frame (zones polygon canvas, motion bbox overlay,
|
|
19563
|
-
* audio waveform). Rendered ordered by `order` (lower first
|
|
19564
|
-
* → bottom; higher → top); the last-registered layer wins
|
|
19565
|
-
* ties.
|
|
19566
|
-
*
|
|
19567
|
-
* 2. **toolbar buttons** — controls surfaced in the player's
|
|
19568
|
-
* always-visible toolbar cluster (next to Intercom + Play/Stop).
|
|
19569
|
-
* Each carries an icon, label, controlled `active` flag, and
|
|
19570
|
-
* a click handler. The host (StreamPanel) renders them
|
|
19571
|
-
* uniformly; tone variants stay simple (`'default' | 'primary'`).
|
|
19572
|
-
*
|
|
19573
|
-
* Lifecycle: hooks register their layer / button on mount and
|
|
19574
|
-
* unregister on unmount, so swapping tabs (e.g. leaving Detection)
|
|
19575
|
-
* automatically tears down the registration. Re-registering with
|
|
19576
|
-
* the same `id` replaces the prior entry — meaning a single hook
|
|
19577
|
-
* call can update its overlay/button props on every render without
|
|
19578
|
-
* leaking entries.
|
|
19579
|
-
*
|
|
19580
|
-
* Provider scope: typically wraps the whole device-detail subtree
|
|
19581
|
-
* so the StreamPanel + every tab share the same registry. One
|
|
19582
|
-
* provider per `deviceId`; switching device IDs unmounts the
|
|
19583
|
-
* provider naturally.
|
|
19584
|
-
*/
|
|
19585
|
-
var PlayerOverlaysStateContext = createSharedContext("camstack:player-overlays-state", null);
|
|
19586
|
-
var PlayerOverlaysActionsContext = createSharedContext("camstack:player-overlays-actions", null);
|
|
19587
|
-
function PlayerOverlaysProvider({ children }) {
|
|
19588
|
-
const [layers, setLayers] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
19589
|
-
const [buttons, setButtons] = (0, react$1.useState)(() => /* @__PURE__ */ new Map());
|
|
19590
|
-
const setLayer = (0, react$1.useCallback)((layer) => {
|
|
19591
|
-
setLayers((prev) => {
|
|
19592
|
-
const next = new Map(prev);
|
|
19593
|
-
next.set(layer.id, layer);
|
|
19594
|
-
return next;
|
|
19595
|
-
});
|
|
19596
|
-
}, []);
|
|
19597
|
-
const removeLayer = (0, react$1.useCallback)((id) => {
|
|
19598
|
-
setLayers((prev) => {
|
|
19599
|
-
if (!prev.has(id)) return prev;
|
|
19600
|
-
const next = new Map(prev);
|
|
19601
|
-
next.delete(id);
|
|
19602
|
-
return next;
|
|
19603
|
-
});
|
|
19604
|
-
}, []);
|
|
19605
|
-
const setButton = (0, react$1.useCallback)((button) => {
|
|
19606
|
-
setButtons((prev) => {
|
|
19607
|
-
const next = new Map(prev);
|
|
19608
|
-
next.set(button.id, button);
|
|
19609
|
-
return next;
|
|
19610
|
-
});
|
|
19611
|
-
}, []);
|
|
19612
|
-
const removeButton = (0, react$1.useCallback)((id) => {
|
|
19613
|
-
setButtons((prev) => {
|
|
19614
|
-
if (!prev.has(id)) return prev;
|
|
19615
|
-
const next = new Map(prev);
|
|
19616
|
-
next.delete(id);
|
|
19617
|
-
return next;
|
|
19618
|
-
});
|
|
19619
|
-
}, []);
|
|
19620
|
-
const stateValue = (0, react$1.useMemo)(() => ({
|
|
19621
|
-
layers,
|
|
19622
|
-
buttons
|
|
19623
|
-
}), [layers, buttons]);
|
|
19624
|
-
const actionsValue = (0, react$1.useMemo)(() => ({
|
|
19625
|
-
setLayer,
|
|
19626
|
-
removeLayer,
|
|
19627
|
-
setButton,
|
|
19628
|
-
removeButton
|
|
19629
|
-
}), [
|
|
19630
|
-
setLayer,
|
|
19631
|
-
removeLayer,
|
|
19632
|
-
setButton,
|
|
19633
|
-
removeButton
|
|
19634
|
-
]);
|
|
19635
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysStateContext.Provider, {
|
|
19636
|
-
value: stateValue,
|
|
19637
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlayerOverlaysActionsContext.Provider, {
|
|
19638
|
-
value: actionsValue,
|
|
19639
|
-
children
|
|
19640
|
-
})
|
|
20929
|
+
fill: "none",
|
|
20930
|
+
stroke: "currentColor",
|
|
20931
|
+
strokeWidth: "2",
|
|
20932
|
+
strokeLinecap: "round",
|
|
20933
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
|
|
20934
|
+
})
|
|
20935
|
+
})]
|
|
20936
|
+
})]
|
|
20937
|
+
})
|
|
20938
|
+
]
|
|
19641
20939
|
});
|
|
19642
20940
|
}
|
|
19643
|
-
|
|
19644
|
-
|
|
19645
|
-
|
|
19646
|
-
|
|
19647
|
-
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
return [...state.layers.values()].sort((a, b) => a.order - b.order);
|
|
19651
|
-
}, [state]);
|
|
19652
|
-
}
|
|
19653
|
-
/** Snapshot of registered toolbar buttons, ordered by `order` (asc). */
|
|
19654
|
-
function usePlayerToolbarButtons() {
|
|
19655
|
-
const state = (0, react$1.useContext)(PlayerOverlaysStateContext);
|
|
19656
|
-
return (0, react$1.useMemo)(() => {
|
|
19657
|
-
if (!state) return [];
|
|
19658
|
-
return [...state.buttons.values()].sort((a, b) => a.order - b.order);
|
|
19659
|
-
}, [state]);
|
|
19660
|
-
}
|
|
19661
|
-
/**
|
|
19662
|
-
* Register an overlay layer for the lifetime of the calling component.
|
|
19663
|
-
* Re-registers on every render with the latest spec; auto-unregisters
|
|
19664
|
-
* on unmount. Pass `null` to skip registration when the layer is
|
|
19665
|
-
* conditionally enabled (the hook still runs every render — keeps
|
|
19666
|
-
* react-hook order stable across spec === null toggles).
|
|
19667
|
-
*
|
|
19668
|
-
* Callers that build the spec inline should memoise it (`useMemo`)
|
|
19669
|
-
* to avoid re-registering on every parent render — context-write
|
|
19670
|
-
* effects depend on referential equality of the spec object.
|
|
19671
|
-
*/
|
|
19672
|
-
function usePlayerOverlayLayer(spec) {
|
|
19673
|
-
const actions = (0, react$1.useContext)(PlayerOverlaysActionsContext);
|
|
19674
|
-
(0, react$1.useEffect)(() => {
|
|
19675
|
-
if (!actions || !spec) return void 0;
|
|
19676
|
-
actions.setLayer(spec);
|
|
19677
|
-
return () => actions.removeLayer(spec.id);
|
|
19678
|
-
}, [actions, spec]);
|
|
20941
|
+
function ToolbarButton$1({ onClick, title, children }) {
|
|
20942
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
20943
|
+
onClick,
|
|
20944
|
+
title,
|
|
20945
|
+
className: "rounded-full p-1.5 text-white/80 hover:text-white hover:bg-white/20 transition-colors",
|
|
20946
|
+
children
|
|
20947
|
+
});
|
|
19679
20948
|
}
|
|
19680
|
-
|
|
19681
|
-
|
|
19682
|
-
|
|
19683
|
-
|
|
19684
|
-
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
20949
|
+
async function waitForIceGathering(pc) {
|
|
20950
|
+
if (pc.iceGatheringState === "complete") return;
|
|
20951
|
+
return new Promise((resolve) => {
|
|
20952
|
+
const handler = () => {
|
|
20953
|
+
if (pc.iceGatheringState === "complete") {
|
|
20954
|
+
pc.removeEventListener("icegatheringstatechange", handler);
|
|
20955
|
+
resolve();
|
|
20956
|
+
}
|
|
20957
|
+
};
|
|
20958
|
+
pc.addEventListener("icegatheringstatechange", handler);
|
|
20959
|
+
setTimeout(resolve, 5e3);
|
|
20960
|
+
});
|
|
19688
20961
|
}
|
|
19689
20962
|
//#endregion
|
|
19690
20963
|
//#region src/composites/stream-panel.tsx
|
|
@@ -22170,7 +23443,7 @@ var LEVEL_OPTIONS = [
|
|
|
22170
23443
|
"error"
|
|
22171
23444
|
];
|
|
22172
23445
|
/** Max live entries kept in the ring buffer. */
|
|
22173
|
-
var MAX_LIVE_ENTRIES$
|
|
23446
|
+
var MAX_LIVE_ENTRIES$1 = 500;
|
|
22174
23447
|
function passesLevel(logLevel, filterLevel) {
|
|
22175
23448
|
if (!filterLevel) return true;
|
|
22176
23449
|
return (LEVEL_SEVERITY[logLevel] ?? 0) >= (LEVEL_SEVERITY[filterLevel] ?? 0);
|
|
@@ -22199,7 +23472,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22199
23472
|
integrationId,
|
|
22200
23473
|
requestId
|
|
22201
23474
|
]);
|
|
22202
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$
|
|
23475
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES$1);
|
|
22203
23476
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22204
23477
|
const liveLogs = buffer.entries;
|
|
22205
23478
|
const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
|
|
@@ -22526,7 +23799,7 @@ function LogStream({ agentId: propsAgentId, addonId: propsAddonId, deviceId: pro
|
|
|
22526
23799
|
}),
|
|
22527
23800
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
22528
23801
|
className: "px-1 py-1 align-top w-5",
|
|
22529
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$
|
|
23802
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton$1, {
|
|
22530
23803
|
copied: copiedRowKey === key,
|
|
22531
23804
|
onCopy: () => copyOne(log, key)
|
|
22532
23805
|
})
|
|
@@ -22565,7 +23838,7 @@ function ScopeBadge$2({ label, value }) {
|
|
|
22565
23838
|
* "did it work" affordance — same UX language the toolbar Copy
|
|
22566
23839
|
* button already uses for its `Copied!` label.
|
|
22567
23840
|
*/
|
|
22568
|
-
function RowCopyButton$
|
|
23841
|
+
function RowCopyButton$1({ copied, onCopy }) {
|
|
22569
23842
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
22570
23843
|
type: "button",
|
|
22571
23844
|
title: "Copy this row",
|
|
@@ -22890,7 +24163,7 @@ function hasContent(entry) {
|
|
|
22890
24163
|
if (cat === _camstack_types.EventCategory.DetectionResult || cat === _camstack_types.EventCategory.PipelineAudioInferenceResult) return summarizeEvent(cat, entry.data) !== null;
|
|
22891
24164
|
return true;
|
|
22892
24165
|
}
|
|
22893
|
-
var MAX_LIVE_ENTRIES
|
|
24166
|
+
var MAX_LIVE_ENTRIES = 300;
|
|
22894
24167
|
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 }) {
|
|
22895
24168
|
const defaultsArray = (0, react$1.useMemo)(() => defaultCategories ?? legacyCategories ?? DEFAULT_EVENT_CATEGORIES, [defaultCategories, legacyCategories]);
|
|
22896
24169
|
const defaultsKey = (0, react$1.useMemo)(() => [...defaultsArray].sort().join(","), [defaultsArray]);
|
|
@@ -22902,7 +24175,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
22902
24175
|
setActiveCategories(new Set(defaultsArray));
|
|
22903
24176
|
}
|
|
22904
24177
|
}, [defaultsKey, defaultsArray]);
|
|
22905
|
-
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES
|
|
24178
|
+
const fallbackBuffer = useLiveBuffer(MAX_LIVE_ENTRIES);
|
|
22906
24179
|
const buffer = externalBuffer ?? fallbackBuffer;
|
|
22907
24180
|
const liveEvents = buffer.entries;
|
|
22908
24181
|
const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
|
|
@@ -23226,7 +24499,7 @@ function EventStream({ agentId, addonId, deviceId, defaultCategories, categories
|
|
|
23226
24499
|
className: "text-foreground-subtle whitespace-nowrap w-[70px] shrink-0 pt-0.5",
|
|
23227
24500
|
children: new Date(event.timestamp).toLocaleTimeString()
|
|
23228
24501
|
}),
|
|
23229
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton
|
|
24502
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowCopyButton, {
|
|
23230
24503
|
copied: copiedRowId === event.id,
|
|
23231
24504
|
onCopy: () => copyOne(event)
|
|
23232
24505
|
}),
|
|
@@ -23416,7 +24689,7 @@ function ScopeBadge$1({ label, value }) {
|
|
|
23416
24689
|
* click doesn't toggle the row's expand state. Flashes a Check
|
|
23417
24690
|
* for ~1s after copy.
|
|
23418
24691
|
*/
|
|
23419
|
-
function RowCopyButton
|
|
24692
|
+
function RowCopyButton({ copied, onCopy }) {
|
|
23420
24693
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
23421
24694
|
type: "button",
|
|
23422
24695
|
title: "Copy this event",
|
|
@@ -23683,162 +24956,137 @@ function StatusBadge$1({ status }) {
|
|
|
23683
24956
|
//#endregion
|
|
23684
24957
|
//#region src/composites/state-values-stream.tsx
|
|
23685
24958
|
/**
|
|
23686
|
-
* StateValuesStream —
|
|
24959
|
+
* StateValuesStream — live current-state tree for ui-library.
|
|
24960
|
+
*
|
|
24961
|
+
* Renders a single device's CURRENT runtime-state as a scrollable,
|
|
24962
|
+
* collapsible hierarchical tree (NOT a chronological change log).
|
|
23687
24963
|
*
|
|
23688
|
-
*
|
|
23689
|
-
*
|
|
23690
|
-
*
|
|
24964
|
+
* - Seed: on mount, `trpc.deviceState.getAllSnapshots` returns the
|
|
24965
|
+
* whole-system snapshot; we pick out this device's entry and build
|
|
24966
|
+
* a `Map<capName, slice>` of its current state.
|
|
24967
|
+
* - Realtime: an `EventCategory.DeviceStateChanged` tRPC subscription
|
|
24968
|
+
* scoped to the device patches each cap's slice into the map in
|
|
24969
|
+
* place — the matching tree node re-renders, no rows are appended.
|
|
24970
|
+
* - Tree: top level = cap names (sorted, expanded by default); each
|
|
24971
|
+
* cap node expands to its slice keys; nested objects/arrays expand
|
|
24972
|
+
* recursively, deep nesting collapsed by default. A per-cap
|
|
24973
|
+
* "updated Ns ago" indicator surfaces realtime activity.
|
|
24974
|
+
* - Filter/search: cap-name multiselect popover + free-text search,
|
|
24975
|
+
* both applied to the tree (filter which caps show; search matches
|
|
24976
|
+
* keys/values anywhere in the slice).
|
|
23691
24977
|
*
|
|
23692
|
-
*
|
|
24978
|
+
* The live subscription is mounted only while this component renders,
|
|
23693
24979
|
* so the parent can mount/unmount it on tab switches and the tRPC
|
|
23694
24980
|
* subscription is torn down automatically when hidden.
|
|
23695
24981
|
*/
|
|
23696
|
-
|
|
23697
|
-
|
|
23698
|
-
|
|
23699
|
-
if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return false;
|
|
23700
|
-
const data = e.data;
|
|
23701
|
-
if (!data || typeof data !== "object") return false;
|
|
23702
|
-
if (data.deviceId !== deviceId) return false;
|
|
23703
|
-
if (typeof data.capName !== "string") return false;
|
|
23704
|
-
if (!data.slice || typeof data.slice !== "object") return false;
|
|
23705
|
-
return true;
|
|
23706
|
-
}
|
|
23707
|
-
function toEntry(raw) {
|
|
24982
|
+
/** Narrow an untrusted live-stream event to a `DeviceStateChanged`
|
|
24983
|
+
* for THIS device, returning the cap name + slice it carries. */
|
|
24984
|
+
function readStateChange(raw, deviceId) {
|
|
23708
24985
|
if (!raw || typeof raw !== "object") return null;
|
|
23709
24986
|
const e = raw;
|
|
23710
|
-
|
|
23711
|
-
const timestamp = typeof e.timestamp === "string" ? e.timestamp : null;
|
|
24987
|
+
if (e.category !== _camstack_types.EventCategory.DeviceStateChanged) return null;
|
|
23712
24988
|
const data = e.data;
|
|
23713
|
-
if (!
|
|
23714
|
-
if (
|
|
24989
|
+
if (!data || typeof data !== "object") return null;
|
|
24990
|
+
if (data.deviceId !== deviceId) return null;
|
|
24991
|
+
if (typeof data.capName !== "string") return null;
|
|
24992
|
+
if (!data.slice || typeof data.slice !== "object") return null;
|
|
23715
24993
|
return {
|
|
23716
|
-
id,
|
|
23717
|
-
timestamp,
|
|
23718
24994
|
capName: data.capName,
|
|
23719
24995
|
slice: data.slice
|
|
23720
24996
|
};
|
|
23721
24997
|
}
|
|
23722
|
-
|
|
24998
|
+
function isSystemSnapshot(raw) {
|
|
24999
|
+
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
25000
|
+
}
|
|
23723
25001
|
/**
|
|
23724
25002
|
* Every device-scoped cap name, sorted alphabetically — same shape
|
|
23725
25003
|
* as `ALL_EVENT_CATEGORIES` in event-stream.tsx so the filter
|
|
23726
25004
|
* surface is symmetric across the three live-streams (Logs, Events,
|
|
23727
|
-
* State). Computed once at module load
|
|
23728
|
-
* full list rather than only the caps that have already emitted at
|
|
23729
|
-
* least one slice change in the current session.
|
|
25005
|
+
* State). Computed once at module load.
|
|
23730
25006
|
*/
|
|
23731
25007
|
var ALL_DEVICE_CAP_NAMES = Object.freeze([...new Set(_camstack_types.ALL_CAPABILITY_DEFINITIONS.filter((c) => c.scope === "device").map((c) => c.name))].sort());
|
|
23732
|
-
|
|
25008
|
+
/** Depth at which tree nodes start collapsed. Cap nodes (depth 0) and
|
|
25009
|
+
* their direct slice keys (depth 1) render expanded; anything nested
|
|
25010
|
+
* deeper opens on demand. */
|
|
25011
|
+
var DEFAULT_EXPAND_DEPTH = 1;
|
|
25012
|
+
function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", onClose, className }) {
|
|
23733
25013
|
const [activeCaps, setActiveCaps] = (0, react$1.useState)((0, react$1.useMemo)(() => new Set(defaultCaps ?? []), [defaultCaps]));
|
|
23734
25014
|
const [searchText, setSearchText] = (0, react$1.useState)("");
|
|
23735
|
-
const [autoScroll, setAutoScroll] = (0, react$1.useState)(true);
|
|
23736
|
-
const [expandedRows, setExpandedRows] = (0, react$1.useState)(/* @__PURE__ */ new Set());
|
|
23737
|
-
const [clearedAt, setClearedAt] = (0, react$1.useState)(0);
|
|
23738
25015
|
const [filterOpen, setFilterOpen] = (0, react$1.useState)(false);
|
|
23739
25016
|
const [filterSearch, setFilterSearch] = (0, react$1.useState)("");
|
|
23740
25017
|
const filterRootRef = (0, react$1.useRef)(null);
|
|
23741
|
-
const
|
|
23742
|
-
const
|
|
23743
|
-
const
|
|
23744
|
-
const scrollRef = (0, react$1.useRef)(null);
|
|
25018
|
+
const [capStates, setCapStates] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
25019
|
+
const [collapseOverrides, setCollapseOverrides] = (0, react$1.useState)(/* @__PURE__ */ new Map());
|
|
25020
|
+
const [, setNowTick] = (0, react$1.useState)(0);
|
|
23745
25021
|
const prevDeviceRef = (0, react$1.useRef)(deviceId);
|
|
23746
25022
|
(0, react$1.useEffect)(() => {
|
|
23747
25023
|
if (prevDeviceRef.current !== deviceId) {
|
|
23748
25024
|
prevDeviceRef.current = deviceId;
|
|
23749
|
-
|
|
23750
|
-
|
|
23751
|
-
setClearedAt(0);
|
|
25025
|
+
setCapStates(/* @__PURE__ */ new Map());
|
|
25026
|
+
setCollapseOverrides(/* @__PURE__ */ new Map());
|
|
23752
25027
|
}
|
|
23753
|
-
}, [deviceId
|
|
23754
|
-
const
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
25028
|
+
}, [deviceId]);
|
|
25029
|
+
const { data: snapshot, isLoading } = trpc.deviceState.getAllSnapshots.useQuery({}, { staleTime: 3e4 });
|
|
25030
|
+
const seededRef = (0, react$1.useRef)(false);
|
|
25031
|
+
(0, react$1.useEffect)(() => {
|
|
25032
|
+
if (seededRef.current) return;
|
|
25033
|
+
if (!isSystemSnapshot(snapshot)) return;
|
|
25034
|
+
const deviceSlices = snapshot[String(deviceId)];
|
|
25035
|
+
seededRef.current = true;
|
|
25036
|
+
if (!deviceSlices) return;
|
|
25037
|
+
const now = Date.now();
|
|
25038
|
+
setCapStates((prev) => {
|
|
25039
|
+
const next = new Map(prev);
|
|
25040
|
+
for (const [capName, slice] of Object.entries(deviceSlices)) {
|
|
25041
|
+
if (next.has(capName)) continue;
|
|
25042
|
+
next.set(capName, {
|
|
25043
|
+
slice,
|
|
25044
|
+
updatedAt: now
|
|
25045
|
+
});
|
|
25046
|
+
}
|
|
25047
|
+
return next;
|
|
25048
|
+
});
|
|
25049
|
+
}, [snapshot, deviceId]);
|
|
23761
25050
|
(0, react$1.useEffect)(() => {
|
|
23762
|
-
|
|
23763
|
-
}, [
|
|
25051
|
+
seededRef.current = false;
|
|
25052
|
+
}, [deviceId]);
|
|
23764
25053
|
trpc.systemEvents.subscribe.useSubscription({
|
|
23765
25054
|
deviceId,
|
|
23766
25055
|
category: _camstack_types.EventCategory.DeviceStateChanged
|
|
23767
25056
|
}, { onData: (raw) => {
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
25057
|
+
const change = readStateChange(raw, deviceId);
|
|
25058
|
+
if (!change) return;
|
|
25059
|
+
setCapStates((prev) => {
|
|
25060
|
+
const next = new Map(prev);
|
|
25061
|
+
next.set(change.capName, {
|
|
25062
|
+
slice: change.slice,
|
|
25063
|
+
updatedAt: Date.now()
|
|
25064
|
+
});
|
|
25065
|
+
return next;
|
|
25066
|
+
});
|
|
23774
25067
|
} });
|
|
23775
|
-
const prevLiveCountRef = (0, react$1.useRef)(liveEvents.length);
|
|
23776
25068
|
(0, react$1.useEffect)(() => {
|
|
23777
|
-
const
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
const
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
else if (!autoScroll && el.scrollTop > 0) {
|
|
23787
|
-
const firstRow = el.firstElementChild?.firstElementChild;
|
|
23788
|
-
const rowHeight = firstRow instanceof HTMLElement ? firstRow.offsetHeight : 28;
|
|
23789
|
-
const newCount = liveEvents.length - prevCount;
|
|
23790
|
-
el.scrollTop += rowHeight * newCount;
|
|
23791
|
-
}
|
|
23792
|
-
}, [liveEvents.length, autoScroll]);
|
|
23793
|
-
const allEntries = (0, react$1.useMemo)(() => {
|
|
23794
|
-
const byId = /* @__PURE__ */ new Map();
|
|
23795
|
-
for (const e of initialEvents ?? []) {
|
|
23796
|
-
const entry = toEntry(e);
|
|
23797
|
-
if (entry) byId.set(entry.id, entry);
|
|
23798
|
-
}
|
|
23799
|
-
for (const e of liveEvents) byId.set(e.id, e);
|
|
23800
|
-
let list = [...byId.values()];
|
|
23801
|
-
if (clearedAt > 0) list = list.filter((e) => new Date(e.timestamp).getTime() > clearedAt);
|
|
23802
|
-
if (activeCaps.size > 0) list = list.filter((e) => activeCaps.has(e.capName));
|
|
25069
|
+
const id = setInterval(() => setNowTick((t) => t + 1), 5e3);
|
|
25070
|
+
return () => clearInterval(id);
|
|
25071
|
+
}, []);
|
|
25072
|
+
const visibleCaps = (0, react$1.useMemo)(() => {
|
|
25073
|
+
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
25074
|
+
for (const capName of capStates.keys()) caps.add(capName);
|
|
25075
|
+
return [...caps].sort();
|
|
25076
|
+
}, [capStates]);
|
|
25077
|
+
const filteredCapNames = (0, react$1.useMemo)(() => {
|
|
23803
25078
|
const needle = searchText.trim().toLowerCase();
|
|
23804
|
-
|
|
23805
|
-
if (
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
try {
|
|
23810
|
-
if (Object.entries(e.slice).map(([k, v]) => `${k}=${formatValue(v)}`).join(" · ").toLowerCase().includes(needle)) return true;
|
|
23811
|
-
} catch {}
|
|
23812
|
-
try {
|
|
23813
|
-
if (JSON.stringify(e.slice).toLowerCase().includes(needle)) return true;
|
|
23814
|
-
} catch {}
|
|
23815
|
-
return false;
|
|
25079
|
+
return [...capStates.keys()].sort().filter((capName) => {
|
|
25080
|
+
if (activeCaps.size > 0 && !activeCaps.has(capName)) return false;
|
|
25081
|
+
if (!needle) return true;
|
|
25082
|
+
if (capName.toLowerCase().includes(needle)) return true;
|
|
25083
|
+
return sliceMatchesSearch(capStates.get(capName)?.slice ?? {}, needle);
|
|
23816
25084
|
});
|
|
23817
|
-
return list.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
23818
25085
|
}, [
|
|
23819
|
-
|
|
23820
|
-
liveEvents,
|
|
25086
|
+
capStates,
|
|
23821
25087
|
activeCaps,
|
|
23822
|
-
searchText
|
|
23823
|
-
clearedAt
|
|
25088
|
+
searchText
|
|
23824
25089
|
]);
|
|
23825
|
-
const visibleCaps = (0, react$1.useMemo)(() => {
|
|
23826
|
-
const caps = new Set(ALL_DEVICE_CAP_NAMES);
|
|
23827
|
-
for (const e of initialEvents ?? []) {
|
|
23828
|
-
const entry = toEntry(e);
|
|
23829
|
-
if (entry) caps.add(entry.capName);
|
|
23830
|
-
}
|
|
23831
|
-
for (const e of liveEvents) caps.add(e.capName);
|
|
23832
|
-
return [...caps].sort();
|
|
23833
|
-
}, [initialEvents, liveEvents]);
|
|
23834
|
-
const toggleRow = (0, react$1.useCallback)((id) => {
|
|
23835
|
-
setExpandedRows((prev) => {
|
|
23836
|
-
const next = new Set(prev);
|
|
23837
|
-
if (next.has(id)) next.delete(id);
|
|
23838
|
-
else next.add(id);
|
|
23839
|
-
return next;
|
|
23840
|
-
});
|
|
23841
|
-
}, []);
|
|
23842
25090
|
const toggleCap = (0, react$1.useCallback)((cap) => {
|
|
23843
25091
|
setActiveCaps((prev) => {
|
|
23844
25092
|
const next = new Set(prev);
|
|
@@ -23851,34 +25099,36 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23851
25099
|
setActiveCaps(new Set(defaultCaps ?? []));
|
|
23852
25100
|
setSearchText("");
|
|
23853
25101
|
}, [defaultCaps]);
|
|
23854
|
-
const handleClear = (0, react$1.useCallback)(() => {
|
|
23855
|
-
setClearedAt(Date.now());
|
|
23856
|
-
buffer.reset();
|
|
23857
|
-
}, [buffer]);
|
|
23858
|
-
const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
|
|
23859
|
-
const [copiedRowId, setCopiedRowId] = (0, react$1.useState)(null);
|
|
23860
|
-
const formatEntryForCopy = (0, react$1.useCallback)((entry) => {
|
|
23861
|
-
return `${new Date(entry.timestamp).toISOString()} [${entry.capName}] slice=${JSON.stringify(entry.slice)}`;
|
|
23862
|
-
}, []);
|
|
23863
|
-
const handleCopyAll = (0, react$1.useCallback)(() => {
|
|
23864
|
-
const lines = allEntries.map(formatEntryForCopy);
|
|
23865
|
-
navigator.clipboard.writeText(lines.join("\n")).then(() => {
|
|
23866
|
-
setCopiedAll(true);
|
|
23867
|
-
setTimeout(() => setCopiedAll(false), 1500);
|
|
23868
|
-
});
|
|
23869
|
-
}, [allEntries, formatEntryForCopy]);
|
|
23870
|
-
const copyOne = (0, react$1.useCallback)((entry) => {
|
|
23871
|
-
navigator.clipboard.writeText(formatEntryForCopy(entry)).then(() => {
|
|
23872
|
-
setCopiedRowId(entry.id);
|
|
23873
|
-
setTimeout(() => setCopiedRowId((prev) => prev === entry.id ? null : prev), 1200);
|
|
23874
|
-
});
|
|
23875
|
-
}, [formatEntryForCopy]);
|
|
23876
25102
|
const handleSelectAllCaps = (0, react$1.useCallback)(() => {
|
|
23877
25103
|
setActiveCaps(new Set(visibleCaps));
|
|
23878
25104
|
}, [visibleCaps]);
|
|
23879
25105
|
const handleSelectNoCaps = (0, react$1.useCallback)(() => {
|
|
23880
25106
|
setActiveCaps(/* @__PURE__ */ new Set());
|
|
23881
25107
|
}, []);
|
|
25108
|
+
const toggleNode = (0, react$1.useCallback)((path, currentlyOpen) => {
|
|
25109
|
+
setCollapseOverrides((prev) => {
|
|
25110
|
+
const next = new Map(prev);
|
|
25111
|
+
next.set(path, !currentlyOpen);
|
|
25112
|
+
return next;
|
|
25113
|
+
});
|
|
25114
|
+
}, []);
|
|
25115
|
+
const isPathOpen = (0, react$1.useCallback)((path, depth) => {
|
|
25116
|
+
const override = collapseOverrides.get(path);
|
|
25117
|
+
if (override !== void 0) return override;
|
|
25118
|
+
return depth < DEFAULT_EXPAND_DEPTH;
|
|
25119
|
+
}, [collapseOverrides]);
|
|
25120
|
+
const [copiedAll, setCopiedAll] = (0, react$1.useState)(false);
|
|
25121
|
+
const handleCopyAll = (0, react$1.useCallback)(() => {
|
|
25122
|
+
const out = {};
|
|
25123
|
+
for (const capName of filteredCapNames) {
|
|
25124
|
+
const state = capStates.get(capName);
|
|
25125
|
+
if (state) out[capName] = state.slice;
|
|
25126
|
+
}
|
|
25127
|
+
navigator.clipboard.writeText(JSON.stringify(out, null, 2)).then(() => {
|
|
25128
|
+
setCopiedAll(true);
|
|
25129
|
+
setTimeout(() => setCopiedAll(false), 1500);
|
|
25130
|
+
});
|
|
25131
|
+
}, [filteredCapNames, capStates]);
|
|
23882
25132
|
const filteredPopoverCaps = (0, react$1.useMemo)(() => {
|
|
23883
25133
|
const needle = filterSearch.trim().toLowerCase();
|
|
23884
25134
|
if (!needle) return visibleCaps;
|
|
@@ -23897,6 +25147,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23897
25147
|
document.addEventListener("mousedown", onDocClick);
|
|
23898
25148
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
23899
25149
|
}, [filterOpen]);
|
|
25150
|
+
const searchNeedle = searchText.trim().toLowerCase();
|
|
23900
25151
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23901
25152
|
className: cn("h-full min-h-0 flex flex-col", className),
|
|
23902
25153
|
onClick: (e) => e.stopPropagation(),
|
|
@@ -23913,18 +25164,6 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23913
25164
|
label: "device",
|
|
23914
25165
|
value: `#${deviceId}`
|
|
23915
25166
|
}),
|
|
23916
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23917
|
-
type: "button",
|
|
23918
|
-
onClick: () => {
|
|
23919
|
-
setAutoScroll((a) => !a);
|
|
23920
|
-
if (!autoScroll && scrollRef.current) scrollRef.current.scrollTo({
|
|
23921
|
-
top: 0,
|
|
23922
|
-
behavior: "smooth"
|
|
23923
|
-
});
|
|
23924
|
-
},
|
|
23925
|
-
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"),
|
|
23926
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ArrowUpToLine, { className: "h-2.5 w-2.5" }), autoScroll ? "Auto" : "Paused"]
|
|
23927
|
-
}),
|
|
23928
25167
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23929
25168
|
className: "relative",
|
|
23930
25169
|
ref: filterRootRef,
|
|
@@ -23932,7 +25171,7 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23932
25171
|
type: "button",
|
|
23933
25172
|
onClick: () => setFilterOpen((v) => !v),
|
|
23934
25173
|
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"),
|
|
23935
|
-
title: "Pick caps to
|
|
25174
|
+
title: "Pick caps to show",
|
|
23936
25175
|
children: [
|
|
23937
25176
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Funnel, { className: "h-2.5 w-2.5" }),
|
|
23938
25177
|
"Filter (",
|
|
@@ -23957,19 +25196,12 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23957
25196
|
title: "Reset filters",
|
|
23958
25197
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RotateCcw, { className: "h-2.5 w-2.5" }), "Reset"]
|
|
23959
25198
|
}),
|
|
23960
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23961
|
-
type: "button",
|
|
23962
|
-
onClick: handleClear,
|
|
23963
|
-
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",
|
|
23964
|
-
title: "Clear visible entries",
|
|
23965
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Trash2, { className: "h-2.5 w-2.5" }), "Clear"]
|
|
23966
|
-
}),
|
|
23967
25199
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23968
25200
|
type: "button",
|
|
23969
25201
|
onClick: handleCopyAll,
|
|
23970
|
-
disabled:
|
|
23971
|
-
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",
|
|
23972
|
-
title: "Copy
|
|
25202
|
+
disabled: filteredCapNames.length === 0,
|
|
25203
|
+
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"),
|
|
25204
|
+
title: "Copy visible state as JSON",
|
|
23973
25205
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-2.5 w-2.5" }), copiedAll ? "Copied!" : "Copy"]
|
|
23974
25206
|
}),
|
|
23975
25207
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -23988,13 +25220,9 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
23988
25220
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-2.5 w-2.5" })
|
|
23989
25221
|
})]
|
|
23990
25222
|
}),
|
|
23991
|
-
|
|
23992
|
-
className: "text-[9px] text-emerald-400 font-medium",
|
|
23993
|
-
children: [
|
|
23994
|
-
"+",
|
|
23995
|
-
liveEvents.length,
|
|
23996
|
-
" live"
|
|
23997
|
-
]
|
|
25223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25224
|
+
className: "text-[9px] text-emerald-400 font-medium inline-flex items-center gap-1",
|
|
25225
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-400 animate-pulse" }), "live"]
|
|
23998
25226
|
})
|
|
23999
25227
|
]
|
|
24000
25228
|
}), onClose && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -24004,90 +25232,172 @@ function StateValuesStream({ deviceId, defaultCaps, maxHeight = "max-h-96", limi
|
|
|
24004
25232
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3.5 w-3.5" })
|
|
24005
25233
|
})]
|
|
24006
25234
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24007
|
-
|
|
24008
|
-
className: cn("flex-1 min-h-0 overflow-auto text-[10px]", maxHeight),
|
|
25235
|
+
className: cn("flex-1 min-h-0 overflow-auto text-[10px] font-mono", maxHeight),
|
|
24009
25236
|
children: [
|
|
24010
|
-
isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24011
|
-
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle",
|
|
24012
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading state
|
|
25237
|
+
isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25238
|
+
className: "flex items-center justify-center gap-2 py-6 text-foreground-subtle font-sans",
|
|
25239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), "Loading device state..."]
|
|
24013
25240
|
}),
|
|
24014
|
-
!isLoading &&
|
|
24015
|
-
className: "py-4 text-center text-foreground-subtle",
|
|
24016
|
-
children: "No state
|
|
25241
|
+
!isLoading && capStates.size === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25242
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25243
|
+
children: "No runtime state"
|
|
24017
25244
|
}),
|
|
24018
|
-
|
|
24019
|
-
className: "
|
|
24020
|
-
children:
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
24026
|
-
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
24031
|
-
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
}),
|
|
24036
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24037
|
-
className: "flex-1 min-w-0",
|
|
24038
|
-
children: [
|
|
24039
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24040
|
-
className: "flex items-center gap-1.5",
|
|
24041
|
-
children: [
|
|
24042
|
-
expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" }),
|
|
24043
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24044
|
-
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",
|
|
24045
|
-
children: entry.capName
|
|
24046
|
-
}),
|
|
24047
|
-
fields.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
24048
|
-
className: "text-[9px] text-foreground-subtle",
|
|
24049
|
-
children: [
|
|
24050
|
-
"+",
|
|
24051
|
-
fields.length - 3,
|
|
24052
|
-
" more"
|
|
24053
|
-
]
|
|
24054
|
-
})
|
|
24055
|
-
]
|
|
24056
|
-
}),
|
|
24057
|
-
summary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
24058
|
-
className: "text-foreground-subtle text-[10px] mt-0.5 ml-5 truncate",
|
|
24059
|
-
children: summary
|
|
24060
|
-
}),
|
|
24061
|
-
expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24062
|
-
className: "mt-1 ml-5 text-[9px] bg-surface rounded px-2 py-1.5 space-y-0.5 font-mono overflow-x-auto",
|
|
24063
|
-
children: fields.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24064
|
-
className: "text-foreground-subtle italic",
|
|
24065
|
-
children: "empty slice"
|
|
24066
|
-
}) : fields.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
24067
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24068
|
-
className: "text-primary/70",
|
|
24069
|
-
children: k
|
|
24070
|
-
}),
|
|
24071
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24072
|
-
className: "text-foreground-subtle",
|
|
24073
|
-
children: ": "
|
|
24074
|
-
}),
|
|
24075
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24076
|
-
className: "text-foreground break-all",
|
|
24077
|
-
children: formatValue(v)
|
|
24078
|
-
})
|
|
24079
|
-
] }, k))
|
|
24080
|
-
})
|
|
24081
|
-
]
|
|
24082
|
-
})
|
|
24083
|
-
]
|
|
24084
|
-
}, entry.id);
|
|
25245
|
+
!isLoading && capStates.size > 0 && filteredCapNames.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25246
|
+
className: "py-4 text-center text-foreground-subtle font-sans",
|
|
25247
|
+
children: "No caps match the filter"
|
|
25248
|
+
}),
|
|
25249
|
+
filteredCapNames.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25250
|
+
className: "py-1",
|
|
25251
|
+
children: filteredCapNames.map((capName) => {
|
|
25252
|
+
const state = capStates.get(capName);
|
|
25253
|
+
if (!state) return null;
|
|
25254
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapNode, {
|
|
25255
|
+
capName,
|
|
25256
|
+
slice: state.slice,
|
|
25257
|
+
updatedAt: state.updatedAt,
|
|
25258
|
+
searchNeedle,
|
|
25259
|
+
isPathOpen,
|
|
25260
|
+
onToggle: toggleNode
|
|
25261
|
+
}, capName);
|
|
24085
25262
|
})
|
|
24086
25263
|
})
|
|
24087
25264
|
]
|
|
24088
25265
|
})]
|
|
24089
25266
|
});
|
|
24090
25267
|
}
|
|
25268
|
+
/** Top-level tree node: one capability. Header carries the cap-name
|
|
25269
|
+
* pill + an "updated Ns ago" indicator; the body expands to the
|
|
25270
|
+
* slice's keys. */
|
|
25271
|
+
function CapNode({ capName, slice, updatedAt, searchNeedle, isPathOpen, onToggle }) {
|
|
25272
|
+
const path = capName;
|
|
25273
|
+
const open = isPathOpen(path, 0);
|
|
25274
|
+
const entries = Object.entries(slice);
|
|
25275
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25276
|
+
className: "flex items-center gap-1.5 px-3 py-1 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25277
|
+
onClick: () => onToggle(path, open),
|
|
25278
|
+
children: [
|
|
25279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
|
|
25280
|
+
open,
|
|
25281
|
+
hasChildren: entries.length > 0
|
|
25282
|
+
}),
|
|
25283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25284
|
+
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",
|
|
25285
|
+
children: capName
|
|
25286
|
+
}),
|
|
25287
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25288
|
+
className: "text-[9px] text-foreground-subtle font-sans",
|
|
25289
|
+
children: [
|
|
25290
|
+
entries.length,
|
|
25291
|
+
" ",
|
|
25292
|
+
entries.length === 1 ? "key" : "keys"
|
|
25293
|
+
]
|
|
25294
|
+
}),
|
|
25295
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
25296
|
+
className: "ml-auto text-[9px] text-foreground-subtle/70 font-sans tabular-nums",
|
|
25297
|
+
children: ["updated ", formatAge(updatedAt)]
|
|
25298
|
+
})
|
|
25299
|
+
]
|
|
25300
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
|
|
25301
|
+
depth: 1,
|
|
25302
|
+
label: "empty slice"
|
|
25303
|
+
}) : entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
|
|
25304
|
+
path: `${path}.${k}`,
|
|
25305
|
+
nodeKey: k,
|
|
25306
|
+
value: v,
|
|
25307
|
+
depth: 1,
|
|
25308
|
+
searchNeedle,
|
|
25309
|
+
isPathOpen,
|
|
25310
|
+
onToggle
|
|
25311
|
+
}, k)) })] });
|
|
25312
|
+
}
|
|
25313
|
+
/** Recursive tree node. A leaf renders `key: value`; an object or
|
|
25314
|
+
* array renders a collapsible branch whose children recurse one
|
|
25315
|
+
* level deeper. */
|
|
25316
|
+
function TreeNode({ path, nodeKey, value, depth, searchNeedle, isPathOpen, onToggle }) {
|
|
25317
|
+
const branch = asBranch(value);
|
|
25318
|
+
const indentStyle = { paddingLeft: `${12 + depth * 14}px` };
|
|
25319
|
+
if (!branch) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25320
|
+
className: "flex items-start gap-1 px-3 py-0.5",
|
|
25321
|
+
style: indentStyle,
|
|
25322
|
+
children: [
|
|
25323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25324
|
+
className: cn("shrink-0", highlightCls(nodeKey, searchNeedle, "text-primary/70")),
|
|
25325
|
+
children: nodeKey
|
|
25326
|
+
}),
|
|
25327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25328
|
+
className: "text-foreground-subtle",
|
|
25329
|
+
children: ":"
|
|
25330
|
+
}),
|
|
25331
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25332
|
+
className: cn("break-all", highlightCls(formatValue(value), searchNeedle, valueCls(value))),
|
|
25333
|
+
children: formatValue(value)
|
|
25334
|
+
})
|
|
25335
|
+
]
|
|
25336
|
+
});
|
|
25337
|
+
const open = isPathOpen(path, depth);
|
|
25338
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
25339
|
+
className: "flex items-center gap-1 px-3 py-0.5 hover:bg-surface-hover/30 cursor-pointer select-none",
|
|
25340
|
+
style: indentStyle,
|
|
25341
|
+
onClick: () => onToggle(path, open),
|
|
25342
|
+
children: [
|
|
25343
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
|
|
25344
|
+
open,
|
|
25345
|
+
hasChildren: branch.entries.length > 0
|
|
25346
|
+
}),
|
|
25347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25348
|
+
className: highlightCls(nodeKey, searchNeedle, "text-primary/70"),
|
|
25349
|
+
children: nodeKey
|
|
25350
|
+
}),
|
|
25351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
25352
|
+
className: "text-foreground-subtle/70 text-[9px]",
|
|
25353
|
+
children: branch.summary
|
|
25354
|
+
})
|
|
25355
|
+
]
|
|
25356
|
+
}), open && (branch.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeafEmpty, {
|
|
25357
|
+
depth: depth + 1,
|
|
25358
|
+
label: branch.kind === "array" ? "empty array" : "empty object"
|
|
25359
|
+
}) : branch.entries.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TreeNode, {
|
|
25360
|
+
path: `${path}.${k}`,
|
|
25361
|
+
nodeKey: k,
|
|
25362
|
+
value: v,
|
|
25363
|
+
depth: depth + 1,
|
|
25364
|
+
searchNeedle,
|
|
25365
|
+
isPathOpen,
|
|
25366
|
+
onToggle
|
|
25367
|
+
}, k)))] });
|
|
25368
|
+
}
|
|
25369
|
+
/** Placeholder row for an object/array/slice with no entries. */
|
|
25370
|
+
function LeafEmpty({ depth, label }) {
|
|
25371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25372
|
+
className: "px-3 py-0.5 text-foreground-subtle/60 italic",
|
|
25373
|
+
style: { paddingLeft: `${12 + depth * 14}px` },
|
|
25374
|
+
children: label
|
|
25375
|
+
});
|
|
25376
|
+
}
|
|
25377
|
+
/** Expand/collapse chevron. Renders a fixed-width spacer when the node
|
|
25378
|
+
* has no children so leaf and branch keys stay column-aligned. */
|
|
25379
|
+
function Chevron({ open, hasChildren }) {
|
|
25380
|
+
if (!hasChildren) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "inline-block w-3 shrink-0" });
|
|
25381
|
+
return open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: "h-3 w-3 text-foreground-subtle shrink-0" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRight, { className: "h-3 w-3 text-foreground-subtle shrink-0" });
|
|
25382
|
+
}
|
|
25383
|
+
/** Classify a value as an expandable branch (plain object or array)
|
|
25384
|
+
* or `null` for a leaf. Arrays expose index → element entries. */
|
|
25385
|
+
function asBranch(value) {
|
|
25386
|
+
if (Array.isArray(value)) return {
|
|
25387
|
+
kind: "array",
|
|
25388
|
+
entries: value.map((v, i) => [String(i), v]),
|
|
25389
|
+
summary: `[${value.length}]`
|
|
25390
|
+
};
|
|
25391
|
+
if (value && typeof value === "object") {
|
|
25392
|
+
const entries = Object.entries(value);
|
|
25393
|
+
return {
|
|
25394
|
+
kind: "object",
|
|
25395
|
+
entries,
|
|
25396
|
+
summary: `{${entries.length}}`
|
|
25397
|
+
};
|
|
25398
|
+
}
|
|
25399
|
+
return null;
|
|
25400
|
+
}
|
|
24091
25401
|
function formatValue(v) {
|
|
24092
25402
|
if (v === null) return "null";
|
|
24093
25403
|
if (v === void 0) return "undefined";
|
|
@@ -24099,9 +25409,46 @@ function formatValue(v) {
|
|
|
24099
25409
|
return String(v);
|
|
24100
25410
|
}
|
|
24101
25411
|
}
|
|
25412
|
+
/** Tailwind colour for a leaf value by JS type. */
|
|
25413
|
+
function valueCls(v) {
|
|
25414
|
+
if (v === null || v === void 0) return "text-foreground-subtle/60";
|
|
25415
|
+
if (typeof v === "number") return "text-amber-400";
|
|
25416
|
+
if (typeof v === "boolean") return v ? "text-emerald-400" : "text-rose-400";
|
|
25417
|
+
return "text-foreground";
|
|
25418
|
+
}
|
|
25419
|
+
/** Append a highlight background when `text` matches the live search
|
|
25420
|
+
* needle, so search hits are visible inside the tree. */
|
|
25421
|
+
function highlightCls(text, needle, base) {
|
|
25422
|
+
if (needle && text.toLowerCase().includes(needle)) return cn(base, "bg-violet-500/20 rounded px-0.5");
|
|
25423
|
+
return base;
|
|
25424
|
+
}
|
|
25425
|
+
/** Deep search a slice — true when `needle` appears in any key or any
|
|
25426
|
+
* stringified leaf value anywhere in the tree. */
|
|
25427
|
+
function sliceMatchesSearch(value, needle) {
|
|
25428
|
+
if (Array.isArray(value)) return value.some((v) => sliceMatchesSearch(v, needle));
|
|
25429
|
+
if (value && typeof value === "object") {
|
|
25430
|
+
for (const [k, v] of Object.entries(value)) {
|
|
25431
|
+
if (k.toLowerCase().includes(needle)) return true;
|
|
25432
|
+
if (sliceMatchesSearch(v, needle)) return true;
|
|
25433
|
+
}
|
|
25434
|
+
return false;
|
|
25435
|
+
}
|
|
25436
|
+
return formatValue(value).toLowerCase().includes(needle);
|
|
25437
|
+
}
|
|
25438
|
+
/** Human "Ns ago" / "Nm ago" age label for the per-cap freshness
|
|
25439
|
+
* indicator. */
|
|
25440
|
+
function formatAge(updatedAt) {
|
|
25441
|
+
const deltaMs = Date.now() - updatedAt;
|
|
25442
|
+
if (deltaMs < 5e3) return "just now";
|
|
25443
|
+
const secs = Math.floor(deltaMs / 1e3);
|
|
25444
|
+
if (secs < 60) return `${secs}s ago`;
|
|
25445
|
+
const mins = Math.floor(secs / 60);
|
|
25446
|
+
if (mins < 60) return `${mins}m ago`;
|
|
25447
|
+
return `${Math.floor(mins / 60)}h ago`;
|
|
25448
|
+
}
|
|
24102
25449
|
function ScopeBadge({ label, value }) {
|
|
24103
25450
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
24104
|
-
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium",
|
|
25451
|
+
className: "text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium font-sans",
|
|
24105
25452
|
children: [
|
|
24106
25453
|
label,
|
|
24107
25454
|
": ",
|
|
@@ -24110,16 +25457,13 @@ function ScopeBadge({ label, value }) {
|
|
|
24110
25457
|
});
|
|
24111
25458
|
}
|
|
24112
25459
|
/**
|
|
24113
|
-
* Cap-name multiselect popover — same shape as
|
|
24114
|
-
*
|
|
24115
|
-
*
|
|
24116
|
-
* Caps don't have icons / styled badges like events do — we render
|
|
24117
|
-
* a flat name + violet-pill bracket so the look stays consistent
|
|
24118
|
-
* with the rest of the StateValuesStream rows.
|
|
25460
|
+
* Cap-name multiselect popover — same shape as `CategoryFilterPopover`
|
|
25461
|
+
* in event-stream.tsx (search box at top, Select All / None / Defaults
|
|
25462
|
+
* buttons, scrollable checkbox list).
|
|
24119
25463
|
*/
|
|
24120
25464
|
function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSelectAll, onSelectNone, onResetDefaults }) {
|
|
24121
25465
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24122
|
-
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",
|
|
25466
|
+
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",
|
|
24123
25467
|
onClick: (e) => e.stopPropagation(),
|
|
24124
25468
|
children: [
|
|
24125
25469
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -24190,24 +25534,6 @@ function CapFilterPopover({ search, onSearchChange, caps, active, onToggle, onSe
|
|
|
24190
25534
|
]
|
|
24191
25535
|
});
|
|
24192
25536
|
}
|
|
24193
|
-
/**
|
|
24194
|
-
* Per-row Copy icon — same shape used in log-stream / event-stream
|
|
24195
|
-
* so the three live-streams' rows are visually symmetric. Stops
|
|
24196
|
-
* propagation so the click doesn't toggle the row's expand state.
|
|
24197
|
-
* Flashes a Check for ~1s on success.
|
|
24198
|
-
*/
|
|
24199
|
-
function RowCopyButton({ copied, onCopy }) {
|
|
24200
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24201
|
-
type: "button",
|
|
24202
|
-
title: "Copy this state change",
|
|
24203
|
-
onClick: (e) => {
|
|
24204
|
-
e.stopPropagation();
|
|
24205
|
-
onCopy();
|
|
24206
|
-
},
|
|
24207
|
-
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"),
|
|
24208
|
-
children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Check, { className: "h-3 w-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, { className: "h-3 w-3" })
|
|
24209
|
-
});
|
|
24210
|
-
}
|
|
24211
25537
|
//#endregion
|
|
24212
25538
|
//#region src/composites/device-activity-panel.tsx
|
|
24213
25539
|
/**
|
|
@@ -24286,303 +25612,99 @@ function DeviceActivityPanel({ deviceId, defaultEventCategories, defaultStateCap
|
|
|
24286
25612
|
showFilters: false,
|
|
24287
25613
|
liveBuffer: logsBuffer
|
|
24288
25614
|
}),
|
|
24289
|
-
tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
|
|
24290
|
-
deviceId,
|
|
24291
|
-
defaultCategories: defaultEventCategories,
|
|
24292
|
-
maxHeight,
|
|
24293
|
-
showCategoryFilter: true,
|
|
24294
|
-
liveBuffer: eventsBuffer
|
|
24295
|
-
}),
|
|
24296
|
-
tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
|
|
24297
|
-
deviceId,
|
|
24298
|
-
defaultCaps: defaultStateCaps,
|
|
24299
|
-
maxHeight,
|
|
24300
|
-
liveBuffer: stateBuffer
|
|
24301
|
-
})
|
|
24302
|
-
]
|
|
24303
|
-
})]
|
|
24304
|
-
});
|
|
24305
|
-
}
|
|
24306
|
-
//#endregion
|
|
24307
|
-
//#region src/composites/confirm-action-button.tsx
|
|
24308
|
-
/**
|
|
24309
|
-
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
24310
|
-
*
|
|
24311
|
-
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
24312
|
-
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
24313
|
-
* normal Button that opens a Dialog; confirming runs the async action,
|
|
24314
|
-
* the trigger spinner-disables itself for the duration, and the dialog
|
|
24315
|
-
* closes on success. Errors surface inline at the dialog's bottom.
|
|
24316
|
-
*
|
|
24317
|
-
* Generic on `TResult` so callers don't have to discard the return
|
|
24318
|
-
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
24319
|
-
* for reboot, RefreshCw for restart).
|
|
24320
|
-
*/
|
|
24321
|
-
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
24322
|
-
const [open, setOpen] = (0, react$1.useState)(false);
|
|
24323
|
-
const [running, setRunning] = (0, react$1.useState)(false);
|
|
24324
|
-
const [error, setError] = (0, react$1.useState)(null);
|
|
24325
|
-
const handleConfirm = async () => {
|
|
24326
|
-
setError(null);
|
|
24327
|
-
setRunning(true);
|
|
24328
|
-
try {
|
|
24329
|
-
const result = await action();
|
|
24330
|
-
onSuccess?.(result);
|
|
24331
|
-
setOpen(false);
|
|
24332
|
-
} catch (err) {
|
|
24333
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
24334
|
-
} finally {
|
|
24335
|
-
setRunning(false);
|
|
24336
|
-
}
|
|
24337
|
-
};
|
|
24338
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
24339
|
-
type: "button",
|
|
24340
|
-
variant: triggerVariant,
|
|
24341
|
-
size,
|
|
24342
|
-
disabled,
|
|
24343
|
-
onClick: () => {
|
|
24344
|
-
setError(null);
|
|
24345
|
-
setOpen(true);
|
|
24346
|
-
},
|
|
24347
|
-
className,
|
|
24348
|
-
children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
24349
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
24350
|
-
open,
|
|
24351
|
-
onOpenChange: (next) => !running && setOpen(next),
|
|
24352
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
|
|
24353
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
|
|
24354
|
-
className: "flex items-center gap-2",
|
|
24355
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
24356
|
-
}), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
|
|
24357
|
-
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24358
|
-
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
24359
|
-
children: error
|
|
24360
|
-
}),
|
|
24361
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
24362
|
-
type: "button",
|
|
24363
|
-
variant: "ghost",
|
|
24364
|
-
size,
|
|
24365
|
-
disabled: running,
|
|
24366
|
-
onClick: () => setOpen(false),
|
|
24367
|
-
children: "Cancel"
|
|
24368
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
24369
|
-
type: "button",
|
|
24370
|
-
variant: confirmVariant,
|
|
24371
|
-
size,
|
|
24372
|
-
disabled: running,
|
|
24373
|
-
onClick: () => {
|
|
24374
|
-
handleConfirm();
|
|
24375
|
-
},
|
|
24376
|
-
children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
24377
|
-
})] })
|
|
24378
|
-
] })
|
|
24379
|
-
})] });
|
|
24380
|
-
}
|
|
24381
|
-
//#endregion
|
|
24382
|
-
//#region src/composites/ptz-overlay.tsx
|
|
24383
|
-
/**
|
|
24384
|
-
* PTZOverlay — pan / tilt / zoom controls.
|
|
24385
|
-
*
|
|
24386
|
-
* Two visual variants driven by `mode`:
|
|
24387
|
-
* - `'overlay'` (default): translucent dark pill positioned bottom-
|
|
24388
|
-
* right of the camera viewport. Used as `extraOverlay` on the
|
|
24389
|
-
* `CameraStreamPlayer` so operators can drive PTZ without leaving
|
|
24390
|
-
* the live view. Opaque background + subtle ring keeps the d-pad
|
|
24391
|
-
* legible against any frame.
|
|
24392
|
-
* - `'panel'`: full-bleed inside a host container (e.g. the floating
|
|
24393
|
-
* PTZ panel in DeviceDetail). No absolute positioning, no inner
|
|
24394
|
-
* wrapper card — the host's chrome is the only frame. Inherits the
|
|
24395
|
-
* surrounding `bg-surface` so the dark theme reads consistently
|
|
24396
|
-
* instead of the previous always-dark-bubble look.
|
|
24397
|
-
*
|
|
24398
|
-
* Interaction model is identical across modes: short tap fires a
|
|
24399
|
-
* discrete pulse (`move`); long press starts continuous motion until
|
|
24400
|
-
* release (`startContinuous` + `stopContinuous` on pointer up).
|
|
24401
|
-
*/
|
|
24402
|
-
function DPadButton({ direction, icon: Icon, disabled, className, variant, onMove, onStart, onStop }) {
|
|
24403
|
-
const [pressedAt, setPressedAt] = (0, react$1.useState)(null);
|
|
24404
|
-
const [continuous, setContinuous] = (0, react$1.useState)(false);
|
|
24405
|
-
const handlePointerDown = (0, react$1.useCallback)((e) => {
|
|
24406
|
-
if (disabled) return;
|
|
24407
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
24408
|
-
setPressedAt(Date.now());
|
|
24409
|
-
setContinuous(false);
|
|
24410
|
-
const timer = setTimeout(() => {
|
|
24411
|
-
setContinuous(true);
|
|
24412
|
-
onStart(direction);
|
|
24413
|
-
}, 250);
|
|
24414
|
-
e.currentTarget.dataset.timer = String(timer);
|
|
24415
|
-
}, [
|
|
24416
|
-
direction,
|
|
24417
|
-
disabled,
|
|
24418
|
-
onStart
|
|
24419
|
-
]);
|
|
24420
|
-
const handlePointerUp = (0, react$1.useCallback)((e) => {
|
|
24421
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24422
|
-
if (timerId) clearTimeout(timerId);
|
|
24423
|
-
e.currentTarget.dataset.timer = "";
|
|
24424
|
-
if (continuous) onStop();
|
|
24425
|
-
else if (pressedAt !== null) onMove(direction);
|
|
24426
|
-
setPressedAt(null);
|
|
24427
|
-
setContinuous(false);
|
|
24428
|
-
}, [
|
|
24429
|
-
continuous,
|
|
24430
|
-
direction,
|
|
24431
|
-
onMove,
|
|
24432
|
-
onStop,
|
|
24433
|
-
pressedAt
|
|
24434
|
-
]);
|
|
24435
|
-
const handlePointerCancel = (0, react$1.useCallback)((e) => {
|
|
24436
|
-
const timerId = Number(e.currentTarget.dataset.timer);
|
|
24437
|
-
if (timerId) clearTimeout(timerId);
|
|
24438
|
-
e.currentTarget.dataset.timer = "";
|
|
24439
|
-
if (continuous) onStop();
|
|
24440
|
-
setPressedAt(null);
|
|
24441
|
-
setContinuous(false);
|
|
24442
|
-
}, [continuous, onStop]);
|
|
24443
|
-
const sizeClass = variant === "panel" ? "h-9 w-9" : "h-7 w-7";
|
|
24444
|
-
const iconSizeClass = variant === "panel" ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24445
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24446
|
-
type: "button",
|
|
24447
|
-
disabled,
|
|
24448
|
-
onPointerDown: handlePointerDown,
|
|
24449
|
-
onPointerUp: handlePointerUp,
|
|
24450
|
-
onPointerCancel: handlePointerCancel,
|
|
24451
|
-
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),
|
|
24452
|
-
title: direction,
|
|
24453
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: iconSizeClass })
|
|
24454
|
-
});
|
|
24455
|
-
}
|
|
24456
|
-
function PTZOverlay({ controls, mode = "overlay", showPresets, showZoom = true, showHome = true, className }) {
|
|
24457
|
-
const { move, startContinuous, stopContinuous, zoom, goHome, goToPreset, presets, busy, error } = controls;
|
|
24458
|
-
const [presetsOpen, setPresetsOpen] = (0, react$1.useState)(false);
|
|
24459
|
-
const presetsVisible = (showPresets ?? presets.length > 0) && presets.length > 0;
|
|
24460
|
-
const isPanel = mode === "panel";
|
|
24461
|
-
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";
|
|
24462
|
-
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");
|
|
24463
|
-
const sideButtonSize = isPanel ? "h-9 w-9" : "h-7 w-7";
|
|
24464
|
-
const sideIconSize = isPanel ? "h-4 w-4" : "h-3.5 w-3.5";
|
|
24465
|
-
const sideButtonHover = isPanel ? "text-foreground hover:bg-surface-hover" : "text-white hover:bg-white/15";
|
|
24466
|
-
const sepClass = isPanel ? "h-12 w-px bg-border mx-1" : "h-12 w-px bg-white/15 mx-1";
|
|
24467
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24468
|
-
className: cn(containerClass, className),
|
|
24469
|
-
children: [error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24470
|
-
className: "rounded bg-danger/90 px-2 py-1 text-[10px] font-medium text-white shadow-lg max-w-[200px] self-center",
|
|
24471
|
-
children: ["PTZ: ", error]
|
|
24472
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24473
|
-
className: cn(rowClass, isPanel && "justify-center"),
|
|
24474
|
-
children: [
|
|
24475
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24476
|
-
className: "grid grid-cols-3 gap-0.5",
|
|
24477
|
-
children: [
|
|
24478
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24479
|
-
"aria-hidden": true,
|
|
24480
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24481
|
-
}),
|
|
24482
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24483
|
-
direction: "up",
|
|
24484
|
-
icon: ArrowUp,
|
|
24485
|
-
variant: mode,
|
|
24486
|
-
onMove: move,
|
|
24487
|
-
onStart: startContinuous,
|
|
24488
|
-
onStop: stopContinuous
|
|
24489
|
-
}),
|
|
24490
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24491
|
-
"aria-hidden": true,
|
|
24492
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24493
|
-
}),
|
|
24494
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24495
|
-
direction: "left",
|
|
24496
|
-
icon: ArrowLeft,
|
|
24497
|
-
variant: mode,
|
|
24498
|
-
onMove: move,
|
|
24499
|
-
onStart: startContinuous,
|
|
24500
|
-
onStop: stopContinuous
|
|
24501
|
-
}),
|
|
24502
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24503
|
-
"aria-hidden": true,
|
|
24504
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24505
|
-
}),
|
|
24506
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24507
|
-
direction: "right",
|
|
24508
|
-
icon: ArrowRight,
|
|
24509
|
-
variant: mode,
|
|
24510
|
-
onMove: move,
|
|
24511
|
-
onStart: startContinuous,
|
|
24512
|
-
onStop: stopContinuous
|
|
24513
|
-
}),
|
|
24514
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24515
|
-
"aria-hidden": true,
|
|
24516
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24517
|
-
}),
|
|
24518
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DPadButton, {
|
|
24519
|
-
direction: "down",
|
|
24520
|
-
icon: ArrowDown,
|
|
24521
|
-
variant: mode,
|
|
24522
|
-
onMove: move,
|
|
24523
|
-
onStart: startContinuous,
|
|
24524
|
-
onStop: stopContinuous
|
|
24525
|
-
}),
|
|
24526
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
24527
|
-
"aria-hidden": true,
|
|
24528
|
-
className: isPanel ? "h-9 w-9" : "h-7 w-7"
|
|
24529
|
-
})
|
|
24530
|
-
]
|
|
24531
|
-
}),
|
|
24532
|
-
(showZoom || showHome) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: sepClass }),
|
|
24533
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
24534
|
-
className: "flex flex-col gap-0.5",
|
|
24535
|
-
children: [showZoom && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24536
|
-
type: "button",
|
|
24537
|
-
onClick: () => zoom("in"),
|
|
24538
|
-
disabled: busy,
|
|
24539
|
-
title: "Zoom in",
|
|
24540
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24541
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomIn, { className: sideIconSize })
|
|
24542
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24543
|
-
type: "button",
|
|
24544
|
-
onClick: () => zoom("out"),
|
|
24545
|
-
disabled: busy,
|
|
24546
|
-
title: "Zoom out",
|
|
24547
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24548
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ZoomOut, { className: sideIconSize })
|
|
24549
|
-
})] }), showHome && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24550
|
-
type: "button",
|
|
24551
|
-
onClick: () => goHome(),
|
|
24552
|
-
disabled: busy,
|
|
24553
|
-
title: "Go home",
|
|
24554
|
-
className: cn("flex items-center justify-center rounded disabled:opacity-40", sideButtonSize, sideButtonHover),
|
|
24555
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(House, { className: sideIconSize })
|
|
24556
|
-
})]
|
|
25615
|
+
tab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventStream, {
|
|
25616
|
+
deviceId,
|
|
25617
|
+
defaultCategories: defaultEventCategories,
|
|
25618
|
+
maxHeight,
|
|
25619
|
+
showCategoryFilter: true,
|
|
25620
|
+
liveBuffer: eventsBuffer
|
|
24557
25621
|
}),
|
|
24558
|
-
|
|
24559
|
-
|
|
24560
|
-
|
|
24561
|
-
|
|
24562
|
-
|
|
24563
|
-
disabled: busy,
|
|
24564
|
-
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"),
|
|
24565
|
-
title: "Presets",
|
|
24566
|
-
children: ["Presets", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDown, { className: cn("h-3 w-3 transition-transform", presetsOpen && "rotate-180") })]
|
|
24567
|
-
}), presetsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
24568
|
-
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"),
|
|
24569
|
-
children: presets.map((p) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
24570
|
-
type: "button",
|
|
24571
|
-
onClick: () => {
|
|
24572
|
-
goToPreset(p.id);
|
|
24573
|
-
setPresetsOpen(false);
|
|
24574
|
-
},
|
|
24575
|
-
disabled: busy,
|
|
24576
|
-
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"),
|
|
24577
|
-
children: p.name || p.id
|
|
24578
|
-
}, p.id))
|
|
24579
|
-
})]
|
|
25622
|
+
tab === "state" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StateValuesStream, {
|
|
25623
|
+
deviceId,
|
|
25624
|
+
defaultCaps: defaultStateCaps,
|
|
25625
|
+
maxHeight,
|
|
25626
|
+
liveBuffer: stateBuffer
|
|
24580
25627
|
})
|
|
24581
25628
|
]
|
|
24582
25629
|
})]
|
|
24583
25630
|
});
|
|
24584
25631
|
}
|
|
24585
25632
|
//#endregion
|
|
25633
|
+
//#region src/composites/confirm-action-button.tsx
|
|
25634
|
+
/**
|
|
25635
|
+
* ConfirmActionButton — destructive-action button with a modal confirm.
|
|
25636
|
+
*
|
|
25637
|
+
* Two-step UX for any operation operators shouldn't trigger by accident:
|
|
25638
|
+
* reboot device, restart addon, regenerate token, etc. The trigger is a
|
|
25639
|
+
* normal Button that opens a Dialog; confirming runs the async action,
|
|
25640
|
+
* the trigger spinner-disables itself for the duration, and the dialog
|
|
25641
|
+
* closes on success. Errors surface inline at the dialog's bottom.
|
|
25642
|
+
*
|
|
25643
|
+
* Generic on `TResult` so callers don't have to discard the return
|
|
25644
|
+
* value. Pass an `icon` to render it inside the trigger (e.g. RotateCw
|
|
25645
|
+
* for reboot, RefreshCw for restart).
|
|
25646
|
+
*/
|
|
25647
|
+
function ConfirmActionButton({ label, icon: Icon, title = "Confirm action", description, confirmLabel, triggerVariant = "outline", confirmVariant = "danger", size = "sm", disabled, className, action, onSuccess }) {
|
|
25648
|
+
const [open, setOpen] = (0, react$1.useState)(false);
|
|
25649
|
+
const [running, setRunning] = (0, react$1.useState)(false);
|
|
25650
|
+
const [error, setError] = (0, react$1.useState)(null);
|
|
25651
|
+
const handleConfirm = async () => {
|
|
25652
|
+
setError(null);
|
|
25653
|
+
setRunning(true);
|
|
25654
|
+
try {
|
|
25655
|
+
const result = await action();
|
|
25656
|
+
onSuccess?.(result);
|
|
25657
|
+
setOpen(false);
|
|
25658
|
+
} catch (err) {
|
|
25659
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
25660
|
+
} finally {
|
|
25661
|
+
setRunning(false);
|
|
25662
|
+
}
|
|
25663
|
+
};
|
|
25664
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
25665
|
+
type: "button",
|
|
25666
|
+
variant: triggerVariant,
|
|
25667
|
+
size,
|
|
25668
|
+
disabled,
|
|
25669
|
+
onClick: () => {
|
|
25670
|
+
setError(null);
|
|
25671
|
+
setOpen(true);
|
|
25672
|
+
},
|
|
25673
|
+
className,
|
|
25674
|
+
children: [Icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3.5 w-3.5" }), label]
|
|
25675
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
|
|
25676
|
+
open,
|
|
25677
|
+
onOpenChange: (next) => !running && setOpen(next),
|
|
25678
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, { children: [
|
|
25679
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogTitle, {
|
|
25680
|
+
className: "flex items-center gap-2",
|
|
25681
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriangleAlert, { className: "h-4 w-4 text-amber-400" }), title]
|
|
25682
|
+
}), typeof description === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: description }) : description] }),
|
|
25683
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
25684
|
+
className: cn("mt-2 px-3 py-2 rounded border border-danger/40 bg-danger/10", "text-xs text-danger"),
|
|
25685
|
+
children: error
|
|
25686
|
+
}),
|
|
25687
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogFooter, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
25688
|
+
type: "button",
|
|
25689
|
+
variant: "ghost",
|
|
25690
|
+
size,
|
|
25691
|
+
disabled: running,
|
|
25692
|
+
onClick: () => setOpen(false),
|
|
25693
|
+
children: "Cancel"
|
|
25694
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
|
|
25695
|
+
type: "button",
|
|
25696
|
+
variant: confirmVariant,
|
|
25697
|
+
size,
|
|
25698
|
+
disabled: running,
|
|
25699
|
+
onClick: () => {
|
|
25700
|
+
handleConfirm();
|
|
25701
|
+
},
|
|
25702
|
+
children: [running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoaderCircle, { className: "h-3.5 w-3.5 animate-spin" }), confirmLabel ?? label]
|
|
25703
|
+
})] })
|
|
25704
|
+
] })
|
|
25705
|
+
})] });
|
|
25706
|
+
}
|
|
25707
|
+
//#endregion
|
|
24586
25708
|
//#region src/composites/snapshot-button.tsx
|
|
24587
25709
|
/**
|
|
24588
25710
|
* SnapshotButton — operator-facing manual snapshot trigger.
|
|
@@ -25489,169 +26611,6 @@ function useDeviceWebrtc(trpc, deviceId, pollIntervalMs = 5e3) {
|
|
|
25489
26611
|
};
|
|
25490
26612
|
}
|
|
25491
26613
|
//#endregion
|
|
25492
|
-
//#region src/hooks/use-ptz.ts
|
|
25493
|
-
/**
|
|
25494
|
-
* usePTZ — PTZ control hook for device-scoped pan / tilt / zoom.
|
|
25495
|
-
*
|
|
25496
|
-
* Wraps the `ptz` capability methods through the canonical
|
|
25497
|
-
* `useDeviceProxy` surface — every call goes through
|
|
25498
|
-
* `dev.ptz?.<method>(...)` with deviceId/nodeId auto-injected. The
|
|
25499
|
-
* hook stays bare-bones around state (`busy`, `error`, `presets`)
|
|
25500
|
-
* so the operator-facing component (PTZOverlay) stays trivial.
|
|
25501
|
-
*
|
|
25502
|
-
* Returns:
|
|
25503
|
-
* - `move(direction)`: discrete one-shot move (cap.move + cap.stop).
|
|
25504
|
-
* Use for short pulse moves triggered by tapping a d-pad button.
|
|
25505
|
-
* - `startContinuous(direction)` / `stopContinuous()`: gesture-driven
|
|
25506
|
-
* continuous motion (`cap.continuousMove` + `cap.stop`). Use for
|
|
25507
|
-
* long-press handlers on touch / mouse-down handlers on desktop.
|
|
25508
|
-
* - `zoom('in' | 'out')`: discrete zoom step.
|
|
25509
|
-
* - `goHome()`: jump to preset 0.
|
|
25510
|
-
* - `presets` + `goToPreset(presetId)`: list and jump to named presets.
|
|
25511
|
-
*
|
|
25512
|
-
* Parametrised by the same `UseDeviceProxyTrpc` shape every other
|
|
25513
|
-
* device hook uses — works under admin-ui (BackendClient.trpc) and
|
|
25514
|
-
* addon pages (AddonPageProps.trpc) alike.
|
|
25515
|
-
*/
|
|
25516
|
-
var DIRECTION_VECTORS = {
|
|
25517
|
-
"up": {
|
|
25518
|
-
pan: 0,
|
|
25519
|
-
tilt: 1
|
|
25520
|
-
},
|
|
25521
|
-
"down": {
|
|
25522
|
-
pan: 0,
|
|
25523
|
-
tilt: -1
|
|
25524
|
-
},
|
|
25525
|
-
"left": {
|
|
25526
|
-
pan: -1,
|
|
25527
|
-
tilt: 0
|
|
25528
|
-
},
|
|
25529
|
-
"right": {
|
|
25530
|
-
pan: 1,
|
|
25531
|
-
tilt: 0
|
|
25532
|
-
},
|
|
25533
|
-
"up-left": {
|
|
25534
|
-
pan: -1,
|
|
25535
|
-
tilt: 1
|
|
25536
|
-
},
|
|
25537
|
-
"up-right": {
|
|
25538
|
-
pan: 1,
|
|
25539
|
-
tilt: 1
|
|
25540
|
-
},
|
|
25541
|
-
"down-left": {
|
|
25542
|
-
pan: -1,
|
|
25543
|
-
tilt: -1
|
|
25544
|
-
},
|
|
25545
|
-
"down-right": {
|
|
25546
|
-
pan: 1,
|
|
25547
|
-
tilt: -1
|
|
25548
|
-
}
|
|
25549
|
-
};
|
|
25550
|
-
function usePTZ(trpc, deviceId, options) {
|
|
25551
|
-
const defaultSpeed = options?.defaultSpeed ?? .5;
|
|
25552
|
-
const pulseMs = options?.pulseMs ?? 250;
|
|
25553
|
-
const enabled = options?.enabled ?? true;
|
|
25554
|
-
const ptz = useDeviceProxy(trpc, enabled ? deviceId : null)?.ptz;
|
|
25555
|
-
const [presets, setPresets] = (0, react$1.useState)([]);
|
|
25556
|
-
const [busy, setBusy] = (0, react$1.useState)(false);
|
|
25557
|
-
const [error, setError] = (0, react$1.useState)(null);
|
|
25558
|
-
const refreshPresets = (0, react$1.useCallback)(async () => {
|
|
25559
|
-
if (!enabled || !ptz) return;
|
|
25560
|
-
try {
|
|
25561
|
-
setPresets(await ptz.getPresets({}));
|
|
25562
|
-
} catch (err) {
|
|
25563
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
25564
|
-
if (msg.includes("provider not available") || msg.includes("no 'ptz' binding")) return;
|
|
25565
|
-
setError(msg);
|
|
25566
|
-
}
|
|
25567
|
-
}, [ptz, enabled]);
|
|
25568
|
-
(0, react$1.useEffect)(() => {
|
|
25569
|
-
refreshPresets();
|
|
25570
|
-
}, [refreshPresets]);
|
|
25571
|
-
const wrap = (0, react$1.useCallback)(async (fn) => {
|
|
25572
|
-
if (!enabled || !ptz) return void 0;
|
|
25573
|
-
setError(null);
|
|
25574
|
-
setBusy(true);
|
|
25575
|
-
try {
|
|
25576
|
-
return await fn();
|
|
25577
|
-
} catch (err) {
|
|
25578
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
25579
|
-
return;
|
|
25580
|
-
} finally {
|
|
25581
|
-
setBusy(false);
|
|
25582
|
-
}
|
|
25583
|
-
}, [enabled, ptz]);
|
|
25584
|
-
return {
|
|
25585
|
-
move: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25586
|
-
if (!ptz) return;
|
|
25587
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25588
|
-
const s = speed ?? defaultSpeed;
|
|
25589
|
-
await wrap(async () => {
|
|
25590
|
-
await ptz.move({
|
|
25591
|
-
pan: v.pan,
|
|
25592
|
-
tilt: v.tilt,
|
|
25593
|
-
speed: s
|
|
25594
|
-
});
|
|
25595
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25596
|
-
await ptz.stop({});
|
|
25597
|
-
});
|
|
25598
|
-
}, [
|
|
25599
|
-
ptz,
|
|
25600
|
-
defaultSpeed,
|
|
25601
|
-
pulseMs,
|
|
25602
|
-
wrap
|
|
25603
|
-
]),
|
|
25604
|
-
startContinuous: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25605
|
-
if (!ptz) return;
|
|
25606
|
-
const v = DIRECTION_VECTORS[direction];
|
|
25607
|
-
const s = speed ?? defaultSpeed;
|
|
25608
|
-
await wrap(() => ptz.continuousMove({
|
|
25609
|
-
pan: v.pan,
|
|
25610
|
-
tilt: v.tilt,
|
|
25611
|
-
speed: s
|
|
25612
|
-
}));
|
|
25613
|
-
}, [
|
|
25614
|
-
ptz,
|
|
25615
|
-
defaultSpeed,
|
|
25616
|
-
wrap
|
|
25617
|
-
]),
|
|
25618
|
-
stopContinuous: (0, react$1.useCallback)(async () => {
|
|
25619
|
-
if (!ptz) return;
|
|
25620
|
-
await wrap(() => ptz.stop({}));
|
|
25621
|
-
}, [ptz, wrap]),
|
|
25622
|
-
zoom: (0, react$1.useCallback)(async (direction, speed) => {
|
|
25623
|
-
if (!ptz) return;
|
|
25624
|
-
const z = direction === "in" ? 1 : -1;
|
|
25625
|
-
const s = speed ?? defaultSpeed;
|
|
25626
|
-
await wrap(async () => {
|
|
25627
|
-
await ptz.move({
|
|
25628
|
-
zoom: z,
|
|
25629
|
-
speed: s
|
|
25630
|
-
});
|
|
25631
|
-
await new Promise((r) => setTimeout(r, pulseMs));
|
|
25632
|
-
await ptz.stop({});
|
|
25633
|
-
});
|
|
25634
|
-
}, [
|
|
25635
|
-
ptz,
|
|
25636
|
-
defaultSpeed,
|
|
25637
|
-
pulseMs,
|
|
25638
|
-
wrap
|
|
25639
|
-
]),
|
|
25640
|
-
goHome: (0, react$1.useCallback)(async () => {
|
|
25641
|
-
if (!ptz) return;
|
|
25642
|
-
await wrap(() => ptz.goHome({}));
|
|
25643
|
-
}, [ptz, wrap]),
|
|
25644
|
-
goToPreset: (0, react$1.useCallback)(async (presetId) => {
|
|
25645
|
-
if (!ptz) return;
|
|
25646
|
-
await wrap(() => ptz.goToPreset({ presetId }));
|
|
25647
|
-
}, [ptz, wrap]),
|
|
25648
|
-
presets,
|
|
25649
|
-
refreshPresets,
|
|
25650
|
-
busy,
|
|
25651
|
-
error
|
|
25652
|
-
};
|
|
25653
|
-
}
|
|
25654
|
-
//#endregion
|
|
25655
26614
|
//#region src/hooks/use-doorbell-events.ts
|
|
25656
26615
|
/**
|
|
25657
26616
|
* useDoorbellEvents — subscribe to doorbell.onPressed across the cluster.
|
|
@@ -25879,6 +26838,7 @@ exports.AppShell = AppShell;
|
|
|
25879
26838
|
exports.AudioClassificationList = AudioClassificationList;
|
|
25880
26839
|
exports.AudioLevelWaveform = AudioLevelWaveform;
|
|
25881
26840
|
exports.AudioWaveform = AudioWaveform;
|
|
26841
|
+
exports.AutotrackSection = AutotrackSection;
|
|
25882
26842
|
exports.BTN_COMPACT = BTN_COMPACT;
|
|
25883
26843
|
exports.BTN_COMPACT_DANGER = BTN_COMPACT_DANGER;
|
|
25884
26844
|
exports.BTN_COMPACT_PRIMARY = BTN_COMPACT_PRIMARY;
|
|
@@ -25941,6 +26901,7 @@ exports.FormField = FormField$1;
|
|
|
25941
26901
|
exports.GRID_GAP = GRID_GAP;
|
|
25942
26902
|
exports.GRID_PAIRED = GRID_PAIRED;
|
|
25943
26903
|
exports.GRID_QUICK_STATS = GRID_QUICK_STATS;
|
|
26904
|
+
exports.HOST_WIDGETS = HOST_WIDGETS;
|
|
25944
26905
|
exports.INPUT_COMPACT = INPUT_COMPACT;
|
|
25945
26906
|
exports.IconButton = IconButton;
|
|
25946
26907
|
exports.ImageSelector = ImageSelector;
|
|
@@ -25953,6 +26914,7 @@ exports.Label = Label;
|
|
|
25953
26914
|
exports.LogStream = LogStream;
|
|
25954
26915
|
exports.LoginForm = LoginForm;
|
|
25955
26916
|
exports.MobileDrawer = MobileDrawer;
|
|
26917
|
+
exports.MotionZonesSettings = MotionZonesSettings;
|
|
25956
26918
|
exports.NodeMultiSelectField = NodeMultiSelectField;
|
|
25957
26919
|
exports.NodePicker = NodePicker;
|
|
25958
26920
|
exports.NodeSelectField = NodeSelectField;
|
|
@@ -25969,6 +26931,7 @@ exports.Popover = Popover;
|
|
|
25969
26931
|
exports.PopoverContent = PopoverContent;
|
|
25970
26932
|
exports.PopoverTrigger = PopoverTrigger;
|
|
25971
26933
|
exports.ProviderBadge = ProviderBadge;
|
|
26934
|
+
exports.PtzPanel = PtzPanel;
|
|
25972
26935
|
exports.QrCode = QrCode;
|
|
25973
26936
|
exports.ResponseLog = ResponseLog;
|
|
25974
26937
|
exports.SECTION_BODY = SECTION_BODY;
|
|
@@ -26026,6 +26989,7 @@ exports.getClassColor = getClassColor;
|
|
|
26026
26989
|
exports.getPhaseVisual = getPhaseVisual;
|
|
26027
26990
|
exports.isFieldVisible = isFieldVisible;
|
|
26028
26991
|
exports.lightColors = require_theme_index.lightColors$1;
|
|
26992
|
+
exports.loadRemoteBundle = loadRemoteBundle;
|
|
26029
26993
|
exports.mirror = mirror;
|
|
26030
26994
|
exports.mountAddonPage = mountAddonPage;
|
|
26031
26995
|
exports.providerIcons = providerIcons;
|
|
@@ -26123,11 +27087,14 @@ exports.useCustomFieldRenderer = useCustomFieldRenderer;
|
|
|
26123
27087
|
exports.useDebouncedString = useDebouncedString;
|
|
26124
27088
|
exports.useDecoderCreateSession = useDecoderCreateSession;
|
|
26125
27089
|
exports.useDecoderDestroySession = useDecoderDestroySession;
|
|
27090
|
+
exports.useDecoderGetFrame = useDecoderGetFrame;
|
|
26126
27091
|
exports.useDecoderGetInfo = useDecoderGetInfo;
|
|
27092
|
+
exports.useDecoderGetShmStats = useDecoderGetShmStats;
|
|
26127
27093
|
exports.useDecoderGetStats = useDecoderGetStats;
|
|
26128
27094
|
exports.useDecoderListActiveSessions = useDecoderListActiveSessions;
|
|
26129
27095
|
exports.useDecoderOpenStream = useDecoderOpenStream;
|
|
26130
27096
|
exports.useDecoderPullFrames = useDecoderPullFrames;
|
|
27097
|
+
exports.useDecoderPullHandles = useDecoderPullHandles;
|
|
26131
27098
|
exports.useDecoderPushPacket = useDecoderPushPacket;
|
|
26132
27099
|
exports.useDecoderReprobeHwaccel = useDecoderReprobeHwaccel;
|
|
26133
27100
|
exports.useDecoderSupportsCodec = useDecoderSupportsCodec;
|
|
@@ -26137,6 +27104,7 @@ exports.useDetectionPipelineGetDeviceLiveContribution = useDetectionPipelineGetD
|
|
|
26137
27104
|
exports.useDetectionPipelineGetDeviceSettingsContribution = useDetectionPipelineGetDeviceSettingsContribution;
|
|
26138
27105
|
exports.useDevShell = useDevShell;
|
|
26139
27106
|
exports.useDevice = useDevice;
|
|
27107
|
+
exports.useDeviceAutotrack = useDeviceAutotrack;
|
|
26140
27108
|
exports.useDeviceBattery = useDeviceBattery;
|
|
26141
27109
|
exports.useDeviceCapability = useDeviceCapability;
|
|
26142
27110
|
exports.useDeviceDetections = useDeviceDetections;
|
|
@@ -26291,6 +27259,9 @@ exports.useMotionGetStatus = useMotionGetStatus;
|
|
|
26291
27259
|
exports.useMotionIsDetected = useMotionIsDetected;
|
|
26292
27260
|
exports.useMotionTriggerGetStatus = useMotionTriggerGetStatus;
|
|
26293
27261
|
exports.useMotionTriggerSetMotionTrigger = useMotionTriggerSetMotionTrigger;
|
|
27262
|
+
exports.useMotionZonesGetOptions = useMotionZonesGetOptions;
|
|
27263
|
+
exports.useMotionZonesGetStatus = useMotionZonesGetStatus;
|
|
27264
|
+
exports.useMotionZonesSetZone = useMotionZonesSetZone;
|
|
26294
27265
|
exports.useMqttBrokerAddBroker = useMqttBrokerAddBroker;
|
|
26295
27266
|
exports.useMqttBrokerGetBrokerConfig = useMqttBrokerGetBrokerConfig;
|
|
26296
27267
|
exports.useMqttBrokerGetStatus = useMqttBrokerGetStatus;
|
|
@@ -26311,6 +27282,7 @@ exports.useNetworkQualityReportClientStats = useNetworkQualityReportClientStats;
|
|
|
26311
27282
|
exports.useNodesClusterAddonStatus = useNodesClusterAddonStatus;
|
|
26312
27283
|
exports.useNodesDeployAddon = useNodesDeployAddon;
|
|
26313
27284
|
exports.useNodesExecuteQuery = useNodesExecuteQuery;
|
|
27285
|
+
exports.useNodesGetCapUsageGraph = useNodesGetCapUsageGraph;
|
|
26314
27286
|
exports.useNodesGetNodeAddons = useNodesGetNodeAddons;
|
|
26315
27287
|
exports.useNodesRenameNode = useNodesRenameNode;
|
|
26316
27288
|
exports.useNodesRestartAddon = useNodesRestartAddon;
|
|
@@ -26433,12 +27405,16 @@ exports.usePtzAutotrackGetStatus = usePtzAutotrackGetStatus;
|
|
|
26433
27405
|
exports.usePtzAutotrackSetEnabled = usePtzAutotrackSetEnabled;
|
|
26434
27406
|
exports.usePtzAutotrackSetSettings = usePtzAutotrackSetSettings;
|
|
26435
27407
|
exports.usePtzContinuousMove = usePtzContinuousMove;
|
|
27408
|
+
exports.usePtzDeletePreset = usePtzDeletePreset;
|
|
27409
|
+
exports.usePtzGetOptions = usePtzGetOptions;
|
|
26436
27410
|
exports.usePtzGetPosition = usePtzGetPosition;
|
|
26437
27411
|
exports.usePtzGetPresets = usePtzGetPresets;
|
|
26438
27412
|
exports.usePtzGetStatus = usePtzGetStatus;
|
|
26439
27413
|
exports.usePtzGoHome = usePtzGoHome;
|
|
26440
27414
|
exports.usePtzGoToPreset = usePtzGoToPreset;
|
|
26441
27415
|
exports.usePtzMove = usePtzMove;
|
|
27416
|
+
exports.usePtzSavePreset = usePtzSavePreset;
|
|
27417
|
+
exports.usePtzSetAutofocus = usePtzSetAutofocus;
|
|
26442
27418
|
exports.usePtzStop = usePtzStop;
|
|
26443
27419
|
exports.useRebootReboot = useRebootReboot;
|
|
26444
27420
|
exports.useRecordingEngineDisable = useRecordingEngineDisable;
|
|
@@ -26462,6 +27438,7 @@ exports.useRecordingEngineUpdateRetentionConfig = useRecordingEngineUpdateRetent
|
|
|
26462
27438
|
exports.useRecordingGetPlaybackUrl = useRecordingGetPlaybackUrl;
|
|
26463
27439
|
exports.useRecordingGetSegments = useRecordingGetSegments;
|
|
26464
27440
|
exports.useRecordingGetThumbnailAt = useRecordingGetThumbnailAt;
|
|
27441
|
+
exports.useRemoteComponent = useRemoteComponent;
|
|
26465
27442
|
exports.useSettingsStoreCount = useSettingsStoreCount;
|
|
26466
27443
|
exports.useSettingsStoreDeclareCollection = useSettingsStoreDeclareCollection;
|
|
26467
27444
|
exports.useSettingsStoreDelete = useSettingsStoreDelete;
|
|
@@ -26503,7 +27480,6 @@ exports.useStorageWriteChunk = useStorageWriteChunk;
|
|
|
26503
27480
|
exports.useStreamBrokerApplyDeviceSettingsPatch = useStreamBrokerApplyDeviceSettingsPatch;
|
|
26504
27481
|
exports.useStreamBrokerAssignProfile = useStreamBrokerAssignProfile;
|
|
26505
27482
|
exports.useStreamBrokerGetAllRtspEntries = useStreamBrokerGetAllRtspEntries;
|
|
26506
|
-
exports.useStreamBrokerGetBroker = useStreamBrokerGetBroker;
|
|
26507
27483
|
exports.useStreamBrokerGetBrokerStats = useStreamBrokerGetBrokerStats;
|
|
26508
27484
|
exports.useStreamBrokerGetDeviceLiveContribution = useStreamBrokerGetDeviceLiveContribution;
|
|
26509
27485
|
exports.useStreamBrokerGetDeviceSettingsContribution = useStreamBrokerGetDeviceSettingsContribution;
|
|
@@ -26518,13 +27494,23 @@ exports.useStreamBrokerListAllCameraStreams = useStreamBrokerListAllCameraStream
|
|
|
26518
27494
|
exports.useStreamBrokerListAllProfileSlots = useStreamBrokerListAllProfileSlots;
|
|
26519
27495
|
exports.useStreamBrokerListClients = useStreamBrokerListClients;
|
|
26520
27496
|
exports.useStreamBrokerPublishCameraStream = useStreamBrokerPublishCameraStream;
|
|
27497
|
+
exports.useStreamBrokerPullAudioChunks = useStreamBrokerPullAudioChunks;
|
|
27498
|
+
exports.useStreamBrokerPullFrameHandles = useStreamBrokerPullFrameHandles;
|
|
26521
27499
|
exports.useStreamBrokerRegenerateRtspToken = useStreamBrokerRegenerateRtspToken;
|
|
26522
27500
|
exports.useStreamBrokerReleaseStreamWithCodec = useStreamBrokerReleaseStreamWithCodec;
|
|
26523
27501
|
exports.useStreamBrokerRestartProfile = useStreamBrokerRestartProfile;
|
|
26524
27502
|
exports.useStreamBrokerRetractCameraStream = useStreamBrokerRetractCameraStream;
|
|
26525
27503
|
exports.useStreamBrokerSetPreBufferDuration = useStreamBrokerSetPreBufferDuration;
|
|
26526
27504
|
exports.useStreamBrokerSetRtspEnabled = useStreamBrokerSetRtspEnabled;
|
|
27505
|
+
exports.useStreamBrokerSubscribeAudioChunks = useStreamBrokerSubscribeAudioChunks;
|
|
27506
|
+
exports.useStreamBrokerSubscribeFrames = useStreamBrokerSubscribeFrames;
|
|
26527
27507
|
exports.useStreamBrokerUnassignProfile = useStreamBrokerUnassignProfile;
|
|
27508
|
+
exports.useStreamBrokerUnsubscribeAudioChunks = useStreamBrokerUnsubscribeAudioChunks;
|
|
27509
|
+
exports.useStreamBrokerUnsubscribeFrames = useStreamBrokerUnsubscribeFrames;
|
|
27510
|
+
exports.useStreamParamsGetConfigSchema = useStreamParamsGetConfigSchema;
|
|
27511
|
+
exports.useStreamParamsGetOptions = useStreamParamsGetOptions;
|
|
27512
|
+
exports.useStreamParamsGetStatus = useStreamParamsGetStatus;
|
|
27513
|
+
exports.useStreamParamsSetProfile = useStreamParamsSetProfile;
|
|
26528
27514
|
exports.useSwitchGetStatus = useSwitchGetStatus;
|
|
26529
27515
|
exports.useSwitchSetState = useSwitchSetState;
|
|
26530
27516
|
exports.useSystem = useSystem;
|
|
@@ -26548,10 +27534,16 @@ exports.useUserManagementDeleteUser = useUserManagementDeleteUser;
|
|
|
26548
27534
|
exports.useUserManagementDisableTotp = useUserManagementDisableTotp;
|
|
26549
27535
|
exports.useUserManagementGetTotpStatus = useUserManagementGetTotpStatus;
|
|
26550
27536
|
exports.useUserManagementListApiKeys = useUserManagementListApiKeys;
|
|
27537
|
+
exports.useUserManagementListOauthSessions = useUserManagementListOauthSessions;
|
|
26551
27538
|
exports.useUserManagementListScopedTokens = useUserManagementListScopedTokens;
|
|
26552
27539
|
exports.useUserManagementListUsers = useUserManagementListUsers;
|
|
27540
|
+
exports.useUserManagementOauthExchangeCode = useUserManagementOauthExchangeCode;
|
|
27541
|
+
exports.useUserManagementOauthIssueCode = useUserManagementOauthIssueCode;
|
|
27542
|
+
exports.useUserManagementOauthRefresh = useUserManagementOauthRefresh;
|
|
27543
|
+
exports.useUserManagementOauthVerifyAccessToken = useUserManagementOauthVerifyAccessToken;
|
|
26553
27544
|
exports.useUserManagementResetPassword = useUserManagementResetPassword;
|
|
26554
27545
|
exports.useUserManagementRevokeApiKey = useUserManagementRevokeApiKey;
|
|
27546
|
+
exports.useUserManagementRevokeOauthSession = useUserManagementRevokeOauthSession;
|
|
26555
27547
|
exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
|
|
26556
27548
|
exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
|
|
26557
27549
|
exports.useUserManagementSetupTotp = useUserManagementSetupTotp;
|