@camstack/ui-library 1.2.347 → 1.2.349
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/CHANGELOG.md +17 -0
- package/dist/composites/device-item/helpers.d.ts +23 -0
- package/dist/composites/device-name/format-device-name.d.ts +4 -3
- package/dist/contexts/unit-preferences-from-server.d.ts +9 -0
- package/dist/contexts/unit-preferences.d.ts +12 -0
- package/dist/generated/system-hooks.d.ts +20 -0
- package/dist/hooks/merge-detections.d.ts +16 -0
- package/dist/hooks/use-account-unit-preferences.d.ts +10 -0
- package/dist/index.cjs +473 -75
- package/dist/index.d.ts +4 -0
- package/dist/index.js +456 -77
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/resolve-sensor-display.d.ts +4 -1
- package/dist/lib/unit-preferences.d.ts +28 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -12528,6 +12528,16 @@ var useRecordingOnboardSetSettings = trpc.recordingOnboard.setSettings.useMutati
|
|
|
12528
12528
|
var useRecordingOnboardGetStatus = trpc.recordingOnboard.getStatus.useQuery;
|
|
12529
12529
|
/** Generated alias around `trpc.recordingSignal.getStatus.useQuery`. */
|
|
12530
12530
|
var useRecordingSignalGetStatus = trpc.recordingSignal.getStatus.useQuery;
|
|
12531
|
+
/** Generated alias around `trpc.remote.getConfig.useQuery`. */
|
|
12532
|
+
var useRemoteGetConfig = trpc.remote.getConfig.useQuery;
|
|
12533
|
+
/** Generated alias around `trpc.remote.setConfig.useMutation`. */
|
|
12534
|
+
var useRemoteSetConfig = trpc.remote.setConfig.useMutation;
|
|
12535
|
+
/** Generated alias around `trpc.remote.listCommands.useQuery`. */
|
|
12536
|
+
var useRemoteListCommands = trpc.remote.listCommands.useQuery;
|
|
12537
|
+
/** Generated alias around `trpc.remote.sendCommand.useMutation`. */
|
|
12538
|
+
var useRemoteSendCommand = trpc.remote.sendCommand.useMutation;
|
|
12539
|
+
/** Generated alias around `trpc.remote.sendMediaCommand.useMutation`. */
|
|
12540
|
+
var useRemoteSendMediaCommand = trpc.remote.sendMediaCommand.useMutation;
|
|
12531
12541
|
/** Generated alias around `trpc.runningState.getStatus.useQuery`. */
|
|
12532
12542
|
var useRunningStateGetStatus = trpc.runningState.getStatus.useQuery;
|
|
12533
12543
|
/** Generated alias around `trpc.sceneMonitor.listScenes.useQuery`. */
|
|
@@ -12814,6 +12824,16 @@ var useSystemSetLoggingSettings = trpc.system.setLoggingSettings.useMutation;
|
|
|
12814
12824
|
var useTamperGetStatus = trpc.tamper.getStatus.useQuery;
|
|
12815
12825
|
/** Generated alias around `trpc.temperatureSensor.getStatus.useQuery`. */
|
|
12816
12826
|
var useTemperatureSensorGetStatus = trpc.temperatureSensor.getStatus.useQuery;
|
|
12827
|
+
/** Generated alias around `trpc.template.list.useQuery`. */
|
|
12828
|
+
var useTemplateList = trpc.template.list.useQuery;
|
|
12829
|
+
/** Generated alias around `trpc.template.get.useQuery`. */
|
|
12830
|
+
var useTemplateGet = trpc.template.get.useQuery;
|
|
12831
|
+
/** Generated alias around `trpc.template.create.useMutation`. */
|
|
12832
|
+
var useTemplateCreate = trpc.template.create.useMutation;
|
|
12833
|
+
/** Generated alias around `trpc.template.update.useMutation`. */
|
|
12834
|
+
var useTemplateUpdate = trpc.template.update.useMutation;
|
|
12835
|
+
/** Generated alias around `trpc.template.delete.useMutation`. */
|
|
12836
|
+
var useTemplateDelete = trpc.template.delete.useMutation;
|
|
12817
12837
|
/** Generated alias around `trpc.terminalSession.listProfiles.useQuery`. */
|
|
12818
12838
|
var useTerminalSessionListProfiles = trpc.terminalSession.listProfiles.useQuery;
|
|
12819
12839
|
/** Generated alias around `trpc.terminalSession.listInstances.useQuery`. */
|
|
@@ -14732,7 +14752,7 @@ Button.displayName = "Button";
|
|
|
14732
14752
|
var INPUT_CLASS$1 = "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";
|
|
14733
14753
|
var CLAIM_POLL_MS = 2e3;
|
|
14734
14754
|
var BARE_CODE = /^[A-Za-z0-9._~-]{8,}$/;
|
|
14735
|
-
function isRecord$
|
|
14755
|
+
function isRecord$5(value) {
|
|
14736
14756
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14737
14757
|
}
|
|
14738
14758
|
function isPresent(value) {
|
|
@@ -14761,7 +14781,7 @@ function choicesOf(value) {
|
|
|
14761
14781
|
if (!Array.isArray(value)) return [];
|
|
14762
14782
|
const out = [];
|
|
14763
14783
|
for (const row of value) {
|
|
14764
|
-
if (!isRecord$
|
|
14784
|
+
if (!isRecord$5(row) || typeof row["value"] !== "string" || typeof row["label"] !== "string") continue;
|
|
14765
14785
|
out.push({
|
|
14766
14786
|
value: row["value"],
|
|
14767
14787
|
label: row["label"],
|
|
@@ -14823,7 +14843,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
|
|
|
14823
14843
|
const timer = setInterval(() => {
|
|
14824
14844
|
if (inFlight.current) return;
|
|
14825
14845
|
onAction(claimAction, field.key, { attemptId }).then((result) => {
|
|
14826
|
-
if (stopped || !isRecord$
|
|
14846
|
+
if (stopped || !isRecord$5(result) || result["pending"] === true) return;
|
|
14827
14847
|
stopped = true;
|
|
14828
14848
|
applyResult(result);
|
|
14829
14849
|
}).catch((err) => {
|
|
@@ -14849,7 +14869,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
|
|
|
14849
14869
|
setError(null);
|
|
14850
14870
|
try {
|
|
14851
14871
|
const result = await onAction(field.beginAction, field.key, {});
|
|
14852
|
-
if (!isRecord$
|
|
14872
|
+
if (!isRecord$5(result) || typeof result["authorizeUrl"] !== "string" || typeof result["attemptId"] !== "string") {
|
|
14853
14873
|
setError(t("uiLibrary.accountSignIn.noLink", "The sign-in page could not be opened. Try again."));
|
|
14854
14874
|
return;
|
|
14855
14875
|
}
|
|
@@ -14877,7 +14897,7 @@ function AccountSignInField({ field, values, disabled, onAction, onChangeMany })
|
|
|
14877
14897
|
attemptId,
|
|
14878
14898
|
callback
|
|
14879
14899
|
});
|
|
14880
|
-
if (!isRecord$
|
|
14900
|
+
if (!isRecord$5(result)) {
|
|
14881
14901
|
setError(t("uiLibrary.accountSignIn.rejected", "The sign-in was not accepted. Start again."));
|
|
14882
14902
|
return;
|
|
14883
14903
|
}
|
|
@@ -20045,7 +20065,12 @@ var DEVICE_NAME_FORMS = [
|
|
|
20045
20065
|
"nested",
|
|
20046
20066
|
"with-location"
|
|
20047
20067
|
];
|
|
20048
|
-
var PARENT_SEPARATORS = [
|
|
20068
|
+
var PARENT_SEPARATORS = [
|
|
20069
|
+
" — ",
|
|
20070
|
+
" - ",
|
|
20071
|
+
": ",
|
|
20072
|
+
" "
|
|
20073
|
+
];
|
|
20049
20074
|
/**
|
|
20050
20075
|
* Readings that are never what a container is. Matches the comment on
|
|
20051
20076
|
* `CONTAINER_CHILD_ROLE_PRIORITY`: battery, temperature, humidity, illuminance,
|
|
@@ -20069,9 +20094,10 @@ function sameDeviceName(left, right) {
|
|
|
20069
20094
|
return foldDeviceName(left) === foldDeviceName(right);
|
|
20070
20095
|
}
|
|
20071
20096
|
/**
|
|
20072
|
-
* Drop a parent prefix. `" — "`
|
|
20073
|
-
*
|
|
20074
|
-
* does not keep a
|
|
20097
|
+
* Drop a parent prefix. The separators `" — "`, `" - "` and `": "` are tried
|
|
20098
|
+
* before a plain space, case-insensitive, so a provider that capitalised the
|
|
20099
|
+
* parent differently still strips, and a dash or colon child does not keep a
|
|
20100
|
+
* dangling mark.
|
|
20075
20101
|
*/
|
|
20076
20102
|
function stripDeviceParentPrefix(name, parentName) {
|
|
20077
20103
|
if (parentName.length === 0) return name;
|
|
@@ -21396,11 +21422,11 @@ function useDeviceSnapshotImage(deviceId, options = {}) {
|
|
|
21396
21422
|
//#region src/composites/cap-settings/camera-grid-layout.ts
|
|
21397
21423
|
/** Smallest rectangle the editor will produce — ffmpeg refuses a zero-area crop. */
|
|
21398
21424
|
var MIN_EXTENT = .02;
|
|
21399
|
-
function isRecord$
|
|
21425
|
+
function isRecord$4(value) {
|
|
21400
21426
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21401
21427
|
}
|
|
21402
21428
|
function readRect(value) {
|
|
21403
|
-
if (!isRecord$
|
|
21429
|
+
if (!isRecord$4(value)) return null;
|
|
21404
21430
|
const { x, y, width, height } = value;
|
|
21405
21431
|
if (typeof x !== "number" || typeof y !== "number" || typeof width !== "number" || typeof height !== "number") return null;
|
|
21406
21432
|
if (x < 0 || y < 0 || width <= 0 || height <= 0) return null;
|
|
@@ -21413,7 +21439,7 @@ function readRect(value) {
|
|
|
21413
21439
|
};
|
|
21414
21440
|
}
|
|
21415
21441
|
function readCell(value) {
|
|
21416
|
-
if (!isRecord$
|
|
21442
|
+
if (!isRecord$4(value)) return null;
|
|
21417
21443
|
const deviceId = value["deviceId"];
|
|
21418
21444
|
if (typeof deviceId !== "number" || !Number.isInteger(deviceId)) return null;
|
|
21419
21445
|
const source = readRect(value["source"]);
|
|
@@ -21426,7 +21452,7 @@ function readCell(value) {
|
|
|
21426
21452
|
};
|
|
21427
21453
|
}
|
|
21428
21454
|
function readProfileOffer(value) {
|
|
21429
|
-
if (!isRecord$
|
|
21455
|
+
if (!isRecord$4(value)) return null;
|
|
21430
21456
|
const profile = value["profile"];
|
|
21431
21457
|
const offered = value["offered"];
|
|
21432
21458
|
const missing = value["missingSources"];
|
|
@@ -21455,7 +21481,7 @@ function readProfileOffer(value) {
|
|
|
21455
21481
|
*/
|
|
21456
21482
|
function parseGridView(raw) {
|
|
21457
21483
|
if (raw === null) return null;
|
|
21458
|
-
if (!isRecord$
|
|
21484
|
+
if (!isRecord$4(raw)) return "unreadable";
|
|
21459
21485
|
const { instanceId, deviceId, name, fps, cells, profiles } = raw;
|
|
21460
21486
|
if (typeof instanceId !== "string" || typeof deviceId !== "number" || typeof name !== "string" || typeof fps !== "number" || !Array.isArray(cells) || !Array.isArray(profiles)) return "unreadable";
|
|
21461
21487
|
const parsed = [];
|
|
@@ -48319,6 +48345,76 @@ function formatNumeric(value, precision) {
|
|
|
48319
48345
|
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
48320
48346
|
}
|
|
48321
48347
|
//#endregion
|
|
48348
|
+
//#region src/lib/unit-preferences.ts
|
|
48349
|
+
/**
|
|
48350
|
+
* The signed-in user's unit preferences (account preference, namespace
|
|
48351
|
+
* `admin.units`). Stored numbers stay in each quantity's base unit (D944); a
|
|
48352
|
+
* preference only picks the label and the conversion at display time.
|
|
48353
|
+
*
|
|
48354
|
+
* Two independent choices: the temperature scale, and metric or imperial for
|
|
48355
|
+
* every other quantity. The default is metric and °C, so a user who never
|
|
48356
|
+
* opens the page sees what they always saw.
|
|
48357
|
+
*/
|
|
48358
|
+
var UNIT_PREFERENCES_NAMESPACE = "admin.units";
|
|
48359
|
+
var TEMPERATURE_SCALES = ["celsius", "fahrenheit"];
|
|
48360
|
+
var DEFAULT_UNIT_PREFERENCES = {
|
|
48361
|
+
system: _camstack_types.DEFAULT_DISPLAY_PREFERENCE,
|
|
48362
|
+
temperature: "celsius"
|
|
48363
|
+
};
|
|
48364
|
+
function isSystem(value) {
|
|
48365
|
+
return _camstack_types.DISPLAY_PREFERENCES.some((candidate) => candidate === value);
|
|
48366
|
+
}
|
|
48367
|
+
function isScale(value) {
|
|
48368
|
+
return TEMPERATURE_SCALES.some((candidate) => candidate === value);
|
|
48369
|
+
}
|
|
48370
|
+
function isRecord$3(value) {
|
|
48371
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48372
|
+
}
|
|
48373
|
+
/** A missing row or field is the default; a field of the wrong shape is unreadable. */
|
|
48374
|
+
function readUnitPreferences(raw) {
|
|
48375
|
+
if (raw === void 0 || raw === null) return {
|
|
48376
|
+
value: DEFAULT_UNIT_PREFERENCES,
|
|
48377
|
+
parsed: true
|
|
48378
|
+
};
|
|
48379
|
+
if (!isRecord$3(raw)) return {
|
|
48380
|
+
value: DEFAULT_UNIT_PREFERENCES,
|
|
48381
|
+
parsed: false
|
|
48382
|
+
};
|
|
48383
|
+
const system = raw["system"];
|
|
48384
|
+
const temperature = raw["temperature"];
|
|
48385
|
+
const systemOk = system === void 0 || isSystem(system);
|
|
48386
|
+
const scaleOk = temperature === void 0 || isScale(temperature);
|
|
48387
|
+
return {
|
|
48388
|
+
value: {
|
|
48389
|
+
system: isSystem(system) ? system : DEFAULT_UNIT_PREFERENCES.system,
|
|
48390
|
+
temperature: isScale(temperature) ? temperature : DEFAULT_UNIT_PREFERENCES.temperature
|
|
48391
|
+
},
|
|
48392
|
+
parsed: systemOk && scaleOk
|
|
48393
|
+
};
|
|
48394
|
+
}
|
|
48395
|
+
/** The display preference to hand `formatReading` for one quantity. */
|
|
48396
|
+
function preferenceForQuantity(preferences, quantity) {
|
|
48397
|
+
if (quantity === "temperature") return preferences.temperature === "fahrenheit" ? "imperial" : "metric";
|
|
48398
|
+
return preferences.system;
|
|
48399
|
+
}
|
|
48400
|
+
//#endregion
|
|
48401
|
+
//#region src/contexts/unit-preferences.tsx
|
|
48402
|
+
/**
|
|
48403
|
+
* The signed-in user's unit preferences, published by context so every
|
|
48404
|
+
* reading renders in the same units without a query of its own. Without a
|
|
48405
|
+
* provider (a spec, a standalone addon page) the default applies: metric, °C.
|
|
48406
|
+
*/
|
|
48407
|
+
var UnitPreferencesContext = (0, react$1.createContext)(DEFAULT_UNIT_PREFERENCES);
|
|
48408
|
+
function UnitPreferencesProvider({ value, children }) {
|
|
48409
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnitPreferencesContext.Provider, {
|
|
48410
|
+
value,
|
|
48411
|
+
children
|
|
48412
|
+
});
|
|
48413
|
+
}
|
|
48414
|
+
function useUnitPreferences() {
|
|
48415
|
+
return (0, react$1.useContext)(UnitPreferencesContext);
|
|
48416
|
+
}
|
|
48417
|
+
//#endregion
|
|
48322
48418
|
//#region src/lib/resolve-sensor-display.ts
|
|
48323
48419
|
function canonUnit(raw) {
|
|
48324
48420
|
if (raw === void 0) return void 0;
|
|
@@ -48337,7 +48433,7 @@ function resolveSensorDisplay(input) {
|
|
|
48337
48433
|
const perCap = input.capName !== void 0 ? input.display?.perCap?.[input.capName] : void 0;
|
|
48338
48434
|
const precision = perCap?.precision ?? input.display?.precision ?? input.roleDefault?.precision ?? input.declarationPrecision;
|
|
48339
48435
|
const quantity = concreteQuantity(input);
|
|
48340
|
-
const preference = input.preference ?? _camstack_types.DEFAULT_DISPLAY_PREFERENCE;
|
|
48436
|
+
const preference = input.preference ?? (input.unitPreferences === void 0 ? _camstack_types.DEFAULT_DISPLAY_PREFERENCE : preferenceForQuantity(input.unitPreferences, quantity));
|
|
48341
48437
|
const base = quantity === void 0 ? void 0 : (0, _camstack_types.baseUnitOf)(quantity);
|
|
48342
48438
|
const preferred = quantity === void 0 ? void 0 : (0, _camstack_types.displayUnitFor)(quantity, preference);
|
|
48343
48439
|
const overrideUnit = canonUnit(perCap?.unit ?? input.display?.unit ?? input.roleDefault?.unit);
|
|
@@ -48552,13 +48648,15 @@ function TextControl({ value, busy, onCommit }) {
|
|
|
48552
48648
|
}
|
|
48553
48649
|
function ControlWidget({ slice, busy, setValueFn, layout, quantity, display, dateTimeShape }) {
|
|
48554
48650
|
const t = useUiText();
|
|
48651
|
+
const unitPreferences = useUnitPreferences();
|
|
48555
48652
|
switch (slice.kind) {
|
|
48556
48653
|
case "numeric": {
|
|
48557
48654
|
const numericValue = typeof slice.value === "number" ? slice.value : Number(slice.value);
|
|
48558
48655
|
const resolved = quantity === void 0 ? void 0 : resolveSensorDisplay({
|
|
48559
48656
|
quantity,
|
|
48560
48657
|
display,
|
|
48561
|
-
capName: "control"
|
|
48658
|
+
capName: "control",
|
|
48659
|
+
unitPreferences
|
|
48562
48660
|
});
|
|
48563
48661
|
const toShown = resolved?.toDisplayValue ?? ((raw) => raw);
|
|
48564
48662
|
const toBase = resolved?.fromDisplayValue ?? ((shown) => shown);
|
|
@@ -49012,6 +49110,7 @@ function renderPresenceLabel$1(state, t) {
|
|
|
49012
49110
|
}
|
|
49013
49111
|
function SensorValueAtom({ trpc, device }) {
|
|
49014
49112
|
const t = useUiText();
|
|
49113
|
+
const unitPreferences = useUnitPreferences();
|
|
49015
49114
|
const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : void 0;
|
|
49016
49115
|
const isBattery = isBatterySensorRole(device.role);
|
|
49017
49116
|
const { data: roleDefaultsData } = useDeviceManagerGetRoleDisplayDefaults({}, { staleTime: 6e4 });
|
|
@@ -49040,7 +49139,8 @@ function SensorValueAtom({ trpc, device }) {
|
|
|
49040
49139
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
49041
49140
|
roleDefault: device.role != null ? roleDefaultsData?.defaults[device.role] : void 0,
|
|
49042
49141
|
quantity: descriptor.quantity ?? "device",
|
|
49043
|
-
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
49142
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity,
|
|
49143
|
+
unitPreferences
|
|
49044
49144
|
}).format(rawValue);
|
|
49045
49145
|
const unit = rendered.unit;
|
|
49046
49146
|
const formatted = rendered.text;
|
|
@@ -49764,6 +49864,7 @@ function BatteryBar({ percent }) {
|
|
|
49764
49864
|
}
|
|
49765
49865
|
function SensorHeroInner({ trpc, device }) {
|
|
49766
49866
|
const t = useUiText();
|
|
49867
|
+
const unitPreferences = useUnitPreferences();
|
|
49767
49868
|
const roleDescriptor = device.role != null ? ROLE_DESCRIPTOR[device.role] : ROLE_DESCRIPTOR["temperature-sensor"];
|
|
49768
49869
|
const isBattery = isBatterySensorRole(device.role);
|
|
49769
49870
|
const { data: roleDefaultsData } = useDeviceManagerGetRoleDisplayDefaults({}, { staleTime: 6e4 });
|
|
@@ -49817,7 +49918,8 @@ function SensorHeroInner({ trpc, device }) {
|
|
|
49817
49918
|
capName: device.role === "numeric-sensor" ? "numeric-sensor" : void 0,
|
|
49818
49919
|
roleDefault: device.role != null ? roleDefaultsData?.defaults?.[device.role] : roleDefaultsData?.defaults?.["temperature-sensor"],
|
|
49819
49920
|
quantity: descriptor.quantity ?? "device",
|
|
49820
|
-
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity
|
|
49921
|
+
deviceQuantity: deviceQuantity === "device" ? void 0 : deviceQuantity,
|
|
49922
|
+
unitPreferences
|
|
49821
49923
|
}).format(rawValue);
|
|
49822
49924
|
const unit = rendered.unit;
|
|
49823
49925
|
const formatted = rendered.text;
|
|
@@ -52338,13 +52440,14 @@ function useWeather(trpc, deviceId) {
|
|
|
52338
52440
|
slice: (d) => d.state.weather
|
|
52339
52441
|
});
|
|
52340
52442
|
}
|
|
52341
|
-
/** Format a base-unit weather reading
|
|
52342
|
-
function formatWeather(value, quantity) {
|
|
52443
|
+
/** Format a base-unit weather reading in the user's units. */
|
|
52444
|
+
function formatWeather(value, quantity, unitPreferences) {
|
|
52343
52445
|
if (value === null) return "—";
|
|
52344
52446
|
const rendered = (0, _camstack_types.formatReading)({
|
|
52345
52447
|
value,
|
|
52346
52448
|
quantity,
|
|
52347
|
-
precision: 0
|
|
52449
|
+
precision: 0,
|
|
52450
|
+
preference: preferenceForQuantity(unitPreferences, quantity)
|
|
52348
52451
|
});
|
|
52349
52452
|
return rendered.unit.length === 0 ? rendered.text : `${rendered.text}${rendered.unit}`;
|
|
52350
52453
|
}
|
|
@@ -52355,6 +52458,7 @@ function formatWeather(value, quantity) {
|
|
|
52355
52458
|
function WeatherInlineControl({ trpc, deviceId }) {
|
|
52356
52459
|
const t = useUiText();
|
|
52357
52460
|
const { slice } = useWeather(trpc, deviceId);
|
|
52461
|
+
const unitPreferences = useUnitPreferences();
|
|
52358
52462
|
if (slice === void 0) return null;
|
|
52359
52463
|
const meta = weatherConditionMeta(slice.condition, t);
|
|
52360
52464
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -52366,7 +52470,7 @@ function WeatherInlineControl({ trpc, deviceId }) {
|
|
|
52366
52470
|
children: inlineGlyph(meta.glyph)
|
|
52367
52471
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
52368
52472
|
className: "tabular-nums",
|
|
52369
|
-
children: formatWeather(slice.temperature, "temperature")
|
|
52473
|
+
children: formatWeather(slice.temperature, "temperature", unitPreferences)
|
|
52370
52474
|
})]
|
|
52371
52475
|
});
|
|
52372
52476
|
}
|
|
@@ -52673,13 +52777,15 @@ function Readout$1({ icon, label, value }) {
|
|
|
52673
52777
|
function WeatherHeroCard({ trpc, deviceId }) {
|
|
52674
52778
|
const t = useUiText();
|
|
52675
52779
|
const { slice } = useWeather(trpc, deviceId);
|
|
52780
|
+
const unitPreferences = useUnitPreferences();
|
|
52676
52781
|
if (slice === void 0) return null;
|
|
52677
52782
|
const meta = weatherConditionMeta(slice.condition, t);
|
|
52678
52783
|
const tint = weatherTint(slice.condition);
|
|
52679
52784
|
const temperature = slice.temperature === null ? void 0 : (0, _camstack_types.formatReading)({
|
|
52680
52785
|
value: slice.temperature,
|
|
52681
52786
|
quantity: "temperature",
|
|
52682
|
-
precision: 0
|
|
52787
|
+
precision: 0,
|
|
52788
|
+
preference: preferenceForQuantity(unitPreferences, "temperature")
|
|
52683
52789
|
});
|
|
52684
52790
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
52685
52791
|
className: "flex flex-col items-stretch gap-4 rounded-lg border border-border bg-surface p-4",
|
|
@@ -52701,7 +52807,8 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
52701
52807
|
className: "text-base text-foreground-subtle",
|
|
52702
52808
|
children: temperature !== void 0 ? temperature.unit : (0, _camstack_types.formatReading)({
|
|
52703
52809
|
value: 0,
|
|
52704
|
-
quantity: "temperature"
|
|
52810
|
+
quantity: "temperature",
|
|
52811
|
+
preference: preferenceForQuantity(unitPreferences, "temperature")
|
|
52705
52812
|
}).unit
|
|
52706
52813
|
}),
|
|
52707
52814
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -52723,12 +52830,12 @@ function WeatherHeroCard({ trpc, deviceId }) {
|
|
|
52723
52830
|
style: { transform: `rotate(${slice.windBearing}deg)` }
|
|
52724
52831
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wind, { className: "h-3.5 w-3.5" }),
|
|
52725
52832
|
label: t("uiLibrary.weatherControl.wind", "Wind"),
|
|
52726
|
-
value: formatWeather(slice.windSpeed, "speed")
|
|
52833
|
+
value: formatWeather(slice.windSpeed, "speed", unitPreferences)
|
|
52727
52834
|
}),
|
|
52728
52835
|
slice.pressure !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Readout$1, {
|
|
52729
52836
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Gauge, { className: "h-3.5 w-3.5" }),
|
|
52730
52837
|
label: t("uiLibrary.weatherControl.pressure", "Pressure"),
|
|
52731
|
-
value: formatWeather(slice.pressure, "pressure")
|
|
52838
|
+
value: formatWeather(slice.pressure, "pressure", unitPreferences)
|
|
52732
52839
|
})
|
|
52733
52840
|
]
|
|
52734
52841
|
})]
|
|
@@ -52968,6 +53075,12 @@ var VOLUME_MAX = 9;
|
|
|
52968
53075
|
/** Amber kibble accent for the bowl fill; destructive red when low. */
|
|
52969
53076
|
var KIBBLE = "#f59e0b";
|
|
52970
53077
|
var KIBBLE_LOW = "#ef4444";
|
|
53078
|
+
/** Hold-to-repeat timing: first repeat after HOLD_DELAY_MS, then every
|
|
53079
|
+
* HOLD_INTERVAL_MS, and after HOLD_ACCEL_AFTER repeats the step grows to 5 g. */
|
|
53080
|
+
var HOLD_DELAY_MS = 350;
|
|
53081
|
+
var HOLD_INTERVAL_MS = 70;
|
|
53082
|
+
var HOLD_ACCEL_AFTER = 8;
|
|
53083
|
+
var HOLD_FAST_STEP = 5;
|
|
52971
53084
|
function clampPortion(grams) {
|
|
52972
53085
|
return Math.min(_camstack_types.PET_FEEDER_MANUAL_FEED_MAX, Math.max(_camstack_types.PET_FEEDER_MANUAL_FEED_MIN, grams));
|
|
52973
53086
|
}
|
|
@@ -52987,9 +53100,9 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
52987
53100
|
slice: (d) => d.state.petFeeder
|
|
52988
53101
|
});
|
|
52989
53102
|
const { display, applyOptimistic } = useOptimisticSlice(slice, { enabled: optimistic ?? true });
|
|
52990
|
-
const [portion, setPortion] = (
|
|
52991
|
-
const [hopper1, setHopper1] = (
|
|
52992
|
-
const [hopper2, setHopper2] = (
|
|
53103
|
+
const [portion, setPortion] = usePortion(display?.lastFeedGrams ?? null);
|
|
53104
|
+
const [hopper1, setHopper1] = usePortion(display?.lastFeedHopper1 ?? null);
|
|
53105
|
+
const [hopper2, setHopper2] = usePortion(display?.lastFeedHopper2 ?? null);
|
|
52993
53106
|
if (cap === void 0) return null;
|
|
52994
53107
|
if (display === void 0) return null;
|
|
52995
53108
|
const busy = !available;
|
|
@@ -53246,7 +53359,8 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53246
53359
|
label: t("uiLibrary.petFeeder.decreaseVolume", "Decrease volume"),
|
|
53247
53360
|
symbol: "−",
|
|
53248
53361
|
disabled: busy || display.volume <= 0,
|
|
53249
|
-
|
|
53362
|
+
repeat: false,
|
|
53363
|
+
onStep: () => {
|
|
53250
53364
|
const next = Math.max(0, display.volume - 1);
|
|
53251
53365
|
applyOptimistic({ volume: next });
|
|
53252
53366
|
cap.setVolume({ level: next });
|
|
@@ -53260,7 +53374,8 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53260
53374
|
label: t("uiLibrary.petFeeder.increaseVolume", "Increase volume"),
|
|
53261
53375
|
symbol: "+",
|
|
53262
53376
|
disabled: busy || display.volume >= VOLUME_MAX,
|
|
53263
|
-
|
|
53377
|
+
repeat: false,
|
|
53378
|
+
onStep: () => {
|
|
53264
53379
|
const next = Math.min(VOLUME_MAX, display.volume + 1);
|
|
53265
53380
|
applyOptimistic({ volume: next });
|
|
53266
53381
|
cap.setVolume({ level: next });
|
|
@@ -53272,6 +53387,22 @@ function PetFeederPanel({ trpc, deviceId, layout, optimistic }) {
|
|
|
53272
53387
|
]
|
|
53273
53388
|
});
|
|
53274
53389
|
}
|
|
53390
|
+
/**
|
|
53391
|
+
* Local manual-feed portion. `null` = untouched: the value follows the device's
|
|
53392
|
+
* remembered last feed (or the default) and picks it up when the slice arrives;
|
|
53393
|
+
* once the operator steps it, the local value wins and stays put.
|
|
53394
|
+
*/
|
|
53395
|
+
function usePortion(remembered) {
|
|
53396
|
+
const [local, setLocal] = (0, react$1.useState)(null);
|
|
53397
|
+
const value = local ?? remembered ?? DEFAULT_PORTION;
|
|
53398
|
+
const valueRef = (0, react$1.useRef)(value);
|
|
53399
|
+
valueRef.current = value;
|
|
53400
|
+
return [value, (0, react$1.useCallback)((update) => {
|
|
53401
|
+
const next = clampPortion(update(valueRef.current));
|
|
53402
|
+
valueRef.current = next;
|
|
53403
|
+
setLocal(next);
|
|
53404
|
+
}, [])];
|
|
53405
|
+
}
|
|
53275
53406
|
/** Labelled control group — a small uppercase header above its control. */
|
|
53276
53407
|
function Field$1({ label, children }) {
|
|
53277
53408
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -53367,8 +53498,9 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
|
|
|
53367
53498
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
|
|
53368
53499
|
label: t("uiLibrary.petFeeder.decreasePortion", "Decrease {{label}} portion", { label }),
|
|
53369
53500
|
symbol: "−",
|
|
53370
|
-
|
|
53371
|
-
|
|
53501
|
+
large: true,
|
|
53502
|
+
disabled: grams <= _camstack_types.PET_FEEDER_MANUAL_FEED_MIN,
|
|
53503
|
+
onStep: (step) => onGrams((current) => current - step)
|
|
53372
53504
|
}),
|
|
53373
53505
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
53374
53506
|
className: "w-12 text-center text-sm font-semibold tabular-nums text-foreground",
|
|
@@ -53380,8 +53512,9 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
|
|
|
53380
53512
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoundStep$1, {
|
|
53381
53513
|
label: t("uiLibrary.petFeeder.increasePortion", "Increase {{label}} portion", { label }),
|
|
53382
53514
|
symbol: "+",
|
|
53383
|
-
|
|
53384
|
-
|
|
53515
|
+
large: true,
|
|
53516
|
+
disabled: grams >= _camstack_types.PET_FEEDER_MANUAL_FEED_MAX,
|
|
53517
|
+
onStep: (step) => onGrams((current) => current + step)
|
|
53385
53518
|
})
|
|
53386
53519
|
]
|
|
53387
53520
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -53397,14 +53530,55 @@ function HopperFeed({ label, grams, busy, onGrams, onFeed }) {
|
|
|
53397
53530
|
})]
|
|
53398
53531
|
});
|
|
53399
53532
|
}
|
|
53400
|
-
/**
|
|
53401
|
-
|
|
53533
|
+
/**
|
|
53534
|
+
* Circular ± button. Pressing steps once at once; holding repeats (and speeds
|
|
53535
|
+
* up) so a large change is one gesture, not dozens of taps. `repeat={false}`
|
|
53536
|
+
* makes it a plain one-shot button (the volume steppers). A keyboard activation
|
|
53537
|
+
* (click without a pointer press, `detail === 0`) steps once.
|
|
53538
|
+
*/
|
|
53539
|
+
function RoundStep$1({ label, symbol, disabled, onStep, repeat = true, large = false }) {
|
|
53540
|
+
const stepRef = (0, react$1.useRef)(onStep);
|
|
53541
|
+
stepRef.current = onStep;
|
|
53542
|
+
const delayRef = (0, react$1.useRef)(null);
|
|
53543
|
+
const loopRef = (0, react$1.useRef)(null);
|
|
53544
|
+
const stop = (0, react$1.useCallback)(() => {
|
|
53545
|
+
if (delayRef.current !== null) clearTimeout(delayRef.current);
|
|
53546
|
+
if (loopRef.current !== null) clearInterval(loopRef.current);
|
|
53547
|
+
delayRef.current = null;
|
|
53548
|
+
loopRef.current = null;
|
|
53549
|
+
}, []);
|
|
53550
|
+
(0, react$1.useEffect)(() => stop, [stop]);
|
|
53551
|
+
(0, react$1.useEffect)(() => {
|
|
53552
|
+
if (disabled) stop();
|
|
53553
|
+
}, [disabled, stop]);
|
|
53554
|
+
const start = () => {
|
|
53555
|
+
if (disabled) return;
|
|
53556
|
+
stepRef.current(1);
|
|
53557
|
+
if (!repeat) return;
|
|
53558
|
+
let repeats = 0;
|
|
53559
|
+
delayRef.current = setTimeout(() => {
|
|
53560
|
+
loopRef.current = setInterval(() => {
|
|
53561
|
+
repeats += 1;
|
|
53562
|
+
stepRef.current(repeats > HOLD_ACCEL_AFTER ? HOLD_FAST_STEP : 1);
|
|
53563
|
+
}, HOLD_INTERVAL_MS);
|
|
53564
|
+
}, HOLD_DELAY_MS);
|
|
53565
|
+
};
|
|
53402
53566
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
53403
53567
|
type: "button",
|
|
53404
53568
|
"aria-label": label,
|
|
53405
53569
|
disabled,
|
|
53406
|
-
|
|
53407
|
-
|
|
53570
|
+
onPointerDown: (e) => {
|
|
53571
|
+
if (e.button !== 0) return;
|
|
53572
|
+
start();
|
|
53573
|
+
},
|
|
53574
|
+
onPointerUp: stop,
|
|
53575
|
+
onPointerLeave: stop,
|
|
53576
|
+
onPointerCancel: stop,
|
|
53577
|
+
onContextMenu: (e) => e.preventDefault(),
|
|
53578
|
+
onClick: (e) => {
|
|
53579
|
+
if (e.detail === 0 && !disabled) stepRef.current(1);
|
|
53580
|
+
},
|
|
53581
|
+
className: cn("flex shrink-0 touch-manipulation select-none items-center justify-center rounded-full border border-border bg-surface leading-none text-foreground-subtle transition-colors hover:bg-surface-hover hover:text-foreground active:bg-surface-hover", large ? "h-9 w-9 text-xl" : "h-7 w-7 text-base", disabled && "opacity-40 cursor-not-allowed"),
|
|
53408
53582
|
children: symbol
|
|
53409
53583
|
});
|
|
53410
53584
|
}
|
|
@@ -57216,17 +57390,13 @@ function deviceLeadIcon(device, roleDefaults) {
|
|
|
57216
57390
|
*
|
|
57217
57391
|
* The `" — "` separator is tried BEFORE the plain space so an Ecowitt child
|
|
57218
57392
|
* (`"Ecowitt Gateway — gateway"`) yields `"gateway"` rather than `"— gateway"`.
|
|
57219
|
-
*
|
|
57220
|
-
*
|
|
57393
|
+
* Matching is case-insensitive and also accepts `" - "` and `": "`. A missing
|
|
57394
|
+
* parent name, a name that doesn't carry the prefix, a name equal to the
|
|
57395
|
+
* parent's, or one with nothing after the prefix is returned verbatim.
|
|
57221
57396
|
*/
|
|
57222
|
-
var CHILD_NAME_SEPARATORS = [" — ", " "];
|
|
57223
57397
|
function stripParentNamePrefix(name, parentName) {
|
|
57224
|
-
|
|
57225
|
-
|
|
57226
|
-
const prefix = `${parentName}${sep}`;
|
|
57227
|
-
if (name.startsWith(prefix)) return name.slice(prefix.length);
|
|
57228
|
-
}
|
|
57229
|
-
return name;
|
|
57398
|
+
const rest = stripDeviceParentPrefix(name, parentName.trim());
|
|
57399
|
+
return rest.length > 0 ? rest : name;
|
|
57230
57400
|
}
|
|
57231
57401
|
/**
|
|
57232
57402
|
* Display name for a child device rendered UNDER its container/parent row.
|
|
@@ -59047,7 +59217,7 @@ function DeviceItem(props) {
|
|
|
59047
59217
|
const flags = resolveFlags(props);
|
|
59048
59218
|
const variant = props.variant ?? "compact";
|
|
59049
59219
|
const view = props.view ?? "card";
|
|
59050
|
-
const nameForm = props.isAccessoryRow ? "nested" : "compact";
|
|
59220
|
+
const nameForm = props.nameForm ?? (props.isAccessoryRow ? "nested" : "compact");
|
|
59051
59221
|
const nameParent = nameForm === "nested" && parent ? {
|
|
59052
59222
|
id: parent.id,
|
|
59053
59223
|
name: parent.name,
|
|
@@ -70368,12 +70538,12 @@ function useHistoryReads(items, window, compare, enabled = true) {
|
|
|
70368
70538
|
* re-applies the same change to the row as it now stands, once. A row this
|
|
70369
70539
|
* client cannot read is never overwritten.
|
|
70370
70540
|
*/
|
|
70371
|
-
var QUERY_KEY = ["preferences", HISTORY_PANELS_NAMESPACE];
|
|
70541
|
+
var QUERY_KEY$1 = ["preferences", HISTORY_PANELS_NAMESPACE];
|
|
70372
70542
|
function useHistoryPanels() {
|
|
70373
70543
|
const client = useSystem().trpcClient;
|
|
70374
70544
|
const qc = (0, _tanstack_react_query.useQueryClient)();
|
|
70375
70545
|
const q = (0, _tanstack_react_query.useQuery)({
|
|
70376
|
-
queryKey: QUERY_KEY,
|
|
70546
|
+
queryKey: QUERY_KEY$1,
|
|
70377
70547
|
queryFn: async () => {
|
|
70378
70548
|
const row = (await client.preferences.get.query({ namespaces: [HISTORY_PANELS_NAMESPACE] })).rows.find((x) => x.namespace === HISTORY_PANELS_NAMESPACE);
|
|
70379
70549
|
return row === void 0 ? null : {
|
|
@@ -70396,7 +70566,7 @@ function useHistoryPanels() {
|
|
|
70396
70566
|
baseRevision: base?.revision ?? 0
|
|
70397
70567
|
});
|
|
70398
70568
|
if (res.status === "saved") {
|
|
70399
|
-
qc.setQueryData(QUERY_KEY, {
|
|
70569
|
+
qc.setQueryData(QUERY_KEY$1, {
|
|
70400
70570
|
value: res.row.value,
|
|
70401
70571
|
revision: res.row.revision
|
|
70402
70572
|
});
|
|
@@ -70406,7 +70576,7 @@ function useHistoryPanels() {
|
|
|
70406
70576
|
value: res.current.value,
|
|
70407
70577
|
revision: res.current.revision
|
|
70408
70578
|
};
|
|
70409
|
-
qc.setQueryData(QUERY_KEY, fresh);
|
|
70579
|
+
qc.setQueryData(QUERY_KEY$1, fresh);
|
|
70410
70580
|
if (!retry) throw new Error("history-panels: the row changed while saving");
|
|
70411
70581
|
await write(fresh, false);
|
|
70412
70582
|
};
|
|
@@ -70620,6 +70790,83 @@ function HistorySkeleton() {
|
|
|
70620
70790
|
});
|
|
70621
70791
|
}
|
|
70622
70792
|
//#endregion
|
|
70793
|
+
//#region src/hooks/use-account-unit-preferences.ts
|
|
70794
|
+
/**
|
|
70795
|
+
* Unit preferences in the signed-in user's ACCOUNT preferences (D894): one
|
|
70796
|
+
* row, namespace `admin.units`, synced to every device signed in as that user.
|
|
70797
|
+
*
|
|
70798
|
+
* A write is a compare-and-set on the revision it was based on; a conflict
|
|
70799
|
+
* re-applies the same change to the row as it now stands, once. A row this
|
|
70800
|
+
* client cannot read is never overwritten.
|
|
70801
|
+
*/
|
|
70802
|
+
var QUERY_KEY = ["preferences", UNIT_PREFERENCES_NAMESPACE];
|
|
70803
|
+
function useAccountUnitPreferences() {
|
|
70804
|
+
const client = useSystem().trpcClient;
|
|
70805
|
+
const qc = (0, _tanstack_react_query.useQueryClient)();
|
|
70806
|
+
const q = (0, _tanstack_react_query.useQuery)({
|
|
70807
|
+
queryKey: QUERY_KEY,
|
|
70808
|
+
queryFn: async () => {
|
|
70809
|
+
const row = (await client.preferences.get.query({ namespaces: [UNIT_PREFERENCES_NAMESPACE] })).rows.find((x) => x.namespace === UNIT_PREFERENCES_NAMESPACE);
|
|
70810
|
+
return row === void 0 ? null : {
|
|
70811
|
+
value: row.value,
|
|
70812
|
+
revision: row.revision
|
|
70813
|
+
};
|
|
70814
|
+
},
|
|
70815
|
+
staleTime: 3e4
|
|
70816
|
+
});
|
|
70817
|
+
const row = q.data ?? null;
|
|
70818
|
+
const parsed = readUnitPreferences(row?.value);
|
|
70819
|
+
const update = (0, react$1.useCallback)(async (change) => {
|
|
70820
|
+
const write = async (base, retry) => {
|
|
70821
|
+
const current = readUnitPreferences(base?.value);
|
|
70822
|
+
if (!current.parsed) throw new Error("unit-preferences: stored row is not readable");
|
|
70823
|
+
const next = {
|
|
70824
|
+
...current.value,
|
|
70825
|
+
...change
|
|
70826
|
+
};
|
|
70827
|
+
const res = await client.preferences.put.mutate({
|
|
70828
|
+
namespace: UNIT_PREFERENCES_NAMESPACE,
|
|
70829
|
+
value: { ...next },
|
|
70830
|
+
baseRevision: base?.revision ?? 0
|
|
70831
|
+
});
|
|
70832
|
+
if (res.status === "saved") {
|
|
70833
|
+
qc.setQueryData(QUERY_KEY, {
|
|
70834
|
+
value: res.row.value,
|
|
70835
|
+
revision: res.row.revision
|
|
70836
|
+
});
|
|
70837
|
+
return;
|
|
70838
|
+
}
|
|
70839
|
+
const fresh = res.current === null ? null : {
|
|
70840
|
+
value: res.current.value,
|
|
70841
|
+
revision: res.current.revision
|
|
70842
|
+
};
|
|
70843
|
+
qc.setQueryData(QUERY_KEY, fresh);
|
|
70844
|
+
if (!retry) throw new Error("unit-preferences: the row changed while saving");
|
|
70845
|
+
await write(fresh, false);
|
|
70846
|
+
};
|
|
70847
|
+
await write(row, true);
|
|
70848
|
+
}, [
|
|
70849
|
+
client,
|
|
70850
|
+
qc,
|
|
70851
|
+
row
|
|
70852
|
+
]);
|
|
70853
|
+
return {
|
|
70854
|
+
status: q.status === "success" ? "ok" : q.status === "error" ? "error" : "pending",
|
|
70855
|
+
value: q.status === "success" ? parsed.value : DEFAULT_UNIT_PREFERENCES,
|
|
70856
|
+
writable: q.status === "success" && parsed.parsed,
|
|
70857
|
+
update
|
|
70858
|
+
};
|
|
70859
|
+
}
|
|
70860
|
+
//#endregion
|
|
70861
|
+
//#region src/contexts/unit-preferences-from-server.tsx
|
|
70862
|
+
function UnitPreferencesFromServer({ children }) {
|
|
70863
|
+
const { value } = useAccountUnitPreferences();
|
|
70864
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UnitPreferencesProvider, {
|
|
70865
|
+
value,
|
|
70866
|
+
children
|
|
70867
|
+
});
|
|
70868
|
+
}
|
|
70869
|
+
//#endregion
|
|
70623
70870
|
//#region src/contexts/zone-editing.tsx
|
|
70624
70871
|
/**
|
|
70625
70872
|
* Per-device zone-editing state shared between the player overlay
|
|
@@ -70783,6 +71030,90 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
70783
71030
|
return useDeviceStateSlice(handleRef.current);
|
|
70784
71031
|
}
|
|
70785
71032
|
//#endregion
|
|
71033
|
+
//#region src/hooks/merge-detections.ts
|
|
71034
|
+
/** Minimum IoU between an enriched root and a raw root to call them one subject
|
|
71035
|
+
* when no trackId pairs them. */
|
|
71036
|
+
var EXTRA_MATCH_IOU_THRESHOLD = .3;
|
|
71037
|
+
function iou(a, b) {
|
|
71038
|
+
const x1 = Math.max(a.x, b.x);
|
|
71039
|
+
const y1 = Math.max(a.y, b.y);
|
|
71040
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width);
|
|
71041
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height);
|
|
71042
|
+
const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
|
|
71043
|
+
const union = a.width * a.height + b.width * b.height - inter;
|
|
71044
|
+
return union > 0 ? inter / union : 0;
|
|
71045
|
+
}
|
|
71046
|
+
function isRoot(d) {
|
|
71047
|
+
return d.kind !== "detail";
|
|
71048
|
+
}
|
|
71049
|
+
function scaleBox(b, sx, sy) {
|
|
71050
|
+
return {
|
|
71051
|
+
x: b.x * sx,
|
|
71052
|
+
y: b.y * sy,
|
|
71053
|
+
width: b.width * sx,
|
|
71054
|
+
height: b.height * sy
|
|
71055
|
+
};
|
|
71056
|
+
}
|
|
71057
|
+
/** The raw root an enriched root describes: same trackId, else best IoU. */
|
|
71058
|
+
function matchRawRoot(enrichedRoot, rawRoots, sx, sy) {
|
|
71059
|
+
const trackId = enrichedRoot.track?.trackId;
|
|
71060
|
+
if (trackId !== void 0) {
|
|
71061
|
+
const byTrack = rawRoots.find((r) => r.track?.trackId === trackId);
|
|
71062
|
+
if (byTrack) return byTrack;
|
|
71063
|
+
}
|
|
71064
|
+
const scaled = scaleBox(enrichedRoot.bbox, sx, sy);
|
|
71065
|
+
let best = null;
|
|
71066
|
+
let bestIou = EXTRA_MATCH_IOU_THRESHOLD;
|
|
71067
|
+
for (const r of rawRoots) {
|
|
71068
|
+
const v = iou(scaled, r.bbox);
|
|
71069
|
+
if (v >= bestIou) {
|
|
71070
|
+
best = r;
|
|
71071
|
+
bestIou = v;
|
|
71072
|
+
}
|
|
71073
|
+
}
|
|
71074
|
+
return best;
|
|
71075
|
+
}
|
|
71076
|
+
function mergeDetections(raw, extras) {
|
|
71077
|
+
if (!extras) return raw;
|
|
71078
|
+
const frameWidth = raw?.frameWidth ?? extras.frameWidth;
|
|
71079
|
+
const frameHeight = raw?.frameHeight ?? extras.frameHeight;
|
|
71080
|
+
const sx = extras.frameWidth > 0 ? frameWidth / extras.frameWidth : 1;
|
|
71081
|
+
const sy = extras.frameHeight > 0 ? frameHeight / extras.frameHeight : 1;
|
|
71082
|
+
const rawDets = raw?.detections ?? [];
|
|
71083
|
+
const rawRoots = rawDets.filter(isRoot);
|
|
71084
|
+
const rawRootFor = /* @__PURE__ */ new Map();
|
|
71085
|
+
const extraLabels = /* @__PURE__ */ new Map();
|
|
71086
|
+
for (const root of extras.detections.filter(isRoot)) {
|
|
71087
|
+
const match = matchRawRoot(root, rawRoots, sx, sy);
|
|
71088
|
+
rawRootFor.set(root.id, match);
|
|
71089
|
+
if (match && root.labels.length > 0) extraLabels.set(match, [...extraLabels.get(match) ?? [], ...root.labels]);
|
|
71090
|
+
}
|
|
71091
|
+
const out = rawDets.map((d) => {
|
|
71092
|
+
const add = extraLabels.get(d);
|
|
71093
|
+
if (!add) return d;
|
|
71094
|
+
const have = new Set(d.labels.map((l) => l.label));
|
|
71095
|
+
const fresh = add.filter((l) => !have.has(l.label));
|
|
71096
|
+
return fresh.length > 0 ? {
|
|
71097
|
+
...d,
|
|
71098
|
+
labels: [...d.labels, ...fresh]
|
|
71099
|
+
} : d;
|
|
71100
|
+
});
|
|
71101
|
+
for (const d of extras.detections) {
|
|
71102
|
+
if (isRoot(d)) continue;
|
|
71103
|
+
const parent = d.parentId !== void 0 ? rawRootFor.get(d.parentId) : void 0;
|
|
71104
|
+
out.push({
|
|
71105
|
+
...d,
|
|
71106
|
+
bbox: scaleBox(d.bbox, sx, sy),
|
|
71107
|
+
...parent ? { parentId: parent.id } : {}
|
|
71108
|
+
});
|
|
71109
|
+
}
|
|
71110
|
+
return {
|
|
71111
|
+
detections: out,
|
|
71112
|
+
frameWidth,
|
|
71113
|
+
frameHeight
|
|
71114
|
+
};
|
|
71115
|
+
}
|
|
71116
|
+
//#endregion
|
|
70786
71117
|
//#region src/hooks/use-device-detections.ts
|
|
70787
71118
|
/**
|
|
70788
71119
|
* useDeviceDetections — subscribes to motion, raw motion, and detection events for a device.
|
|
@@ -70791,12 +71122,18 @@ function useDeviceCapSlice(trpc, deviceId, capName) {
|
|
|
70791
71122
|
* admin-ui (BackendClient.trpc) and addon pages (AddonPageProps.trpc).
|
|
70792
71123
|
*/
|
|
70793
71124
|
var MOTION_FADE_MS = 500;
|
|
70794
|
-
/**
|
|
70795
|
-
*
|
|
70796
|
-
* frames are ignored (post-analysis is the richer source; raw remains the
|
|
70797
|
-
* fallback when post-analysis is absent). Keeping the two equal means raw
|
|
70798
|
-
* resumes exactly when the enriched display fades — no blank gap. */
|
|
71125
|
+
/** Fade of each detection source (raw roots, enriched extras), counted from the
|
|
71126
|
+
* frame's capture time so a late frame is not kept for the full window. */
|
|
70799
71127
|
var DETECTION_FADE_MS = 800;
|
|
71128
|
+
/** A frame older than this at arrival is replayed pre-roll and never drawn.
|
|
71129
|
+
* Same meaning as the server's OVERLAY_MAX_FRAME_AGE_MS. */
|
|
71130
|
+
var OVERLAY_MAX_FRAME_AGE_MS = 1e3;
|
|
71131
|
+
/**
|
|
71132
|
+
* The most of a frame's age (arrival minus its capture timestamp) the fade
|
|
71133
|
+
* deducts. Bounded so a skewed server/browser clock can never make boxes vanish
|
|
71134
|
+
* on arrival.
|
|
71135
|
+
*/
|
|
71136
|
+
var MAX_FRAME_AGE_FADE_DEDUCTION_MS = DETECTION_FADE_MS / 2;
|
|
70800
71137
|
/** Helper: subscribe to live.onEvent without optional chaining on subscribe() */
|
|
70801
71138
|
function safeSubscribe(trpc, category, onData) {
|
|
70802
71139
|
try {
|
|
@@ -70819,8 +71156,26 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
70819
71156
|
const [phase, setPhase] = (0, react$1.useState)("watching");
|
|
70820
71157
|
const motionTimer = (0, react$1.useRef)(void 0);
|
|
70821
71158
|
const motionRawTimer = (0, react$1.useRef)(void 0);
|
|
70822
|
-
const
|
|
70823
|
-
const
|
|
71159
|
+
const rawTimer = (0, react$1.useRef)(void 0);
|
|
71160
|
+
const extrasTimer = (0, react$1.useRef)(void 0);
|
|
71161
|
+
/** Live part of each source: raw roots and enriched extras fade independently. */
|
|
71162
|
+
const rawPart = (0, react$1.useRef)(null);
|
|
71163
|
+
const extrasPart = (0, react$1.useRef)(null);
|
|
71164
|
+
/** Capture timestamp of the newest frame shown, per source; older arrivals are dropped. */
|
|
71165
|
+
const lastRawTs = (0, react$1.useRef)(0);
|
|
71166
|
+
const lastExtrasTs = (0, react$1.useRef)(0);
|
|
71167
|
+
const publish = (0, react$1.useCallback)(() => {
|
|
71168
|
+
setDetection(mergeDetections(rawPart.current, extrasPart.current));
|
|
71169
|
+
}, []);
|
|
71170
|
+
/** Admit a frame: null = rejected (stale or out of order), else its fade in ms. */
|
|
71171
|
+
const admit = (0, react$1.useCallback)((frameTs, lastTs) => {
|
|
71172
|
+
if (frameTs !== null) {
|
|
71173
|
+
if (Date.now() - frameTs > OVERLAY_MAX_FRAME_AGE_MS) return null;
|
|
71174
|
+
if (frameTs < lastTs.current) return null;
|
|
71175
|
+
lastTs.current = frameTs;
|
|
71176
|
+
}
|
|
71177
|
+
return DETECTION_FADE_MS - (frameTs === null ? 0 : Math.min(Math.max(Date.now() - frameTs, 0), MAX_FRAME_AGE_FADE_DEDUCTION_MS));
|
|
71178
|
+
}, []);
|
|
70824
71179
|
(0, react$1.useEffect)(() => {
|
|
70825
71180
|
if (deviceId === null) return;
|
|
70826
71181
|
const sub = safeSubscribe(trpc, "detection.motion-analysis", (event) => {
|
|
@@ -70869,48 +71224,72 @@ function useDeviceDetections(trpc, deviceId) {
|
|
|
70869
71224
|
};
|
|
70870
71225
|
}, [trpc, deviceId]);
|
|
70871
71226
|
(0, react$1.useEffect)(() => {
|
|
70872
|
-
|
|
71227
|
+
extrasPart.current = null;
|
|
71228
|
+
rawPart.current = null;
|
|
71229
|
+
lastExtrasTs.current = 0;
|
|
71230
|
+
lastRawTs.current = 0;
|
|
71231
|
+
setDetection(null);
|
|
70873
71232
|
if (deviceId === null) return;
|
|
70874
71233
|
const sub = safeSubscribe(trpc, "pipeline-analytics.frame-tracked", (event) => {
|
|
70875
71234
|
const data = event?.data;
|
|
70876
71235
|
if (!data || data.deviceId !== deviceId) return;
|
|
70877
71236
|
if (!data.detections?.length) return;
|
|
70878
71237
|
if (typeof data.detections[0]?.kind !== "string") return;
|
|
70879
|
-
|
|
70880
|
-
|
|
71238
|
+
const fadeInMs = admit(typeof data.timestamp === "number" ? data.timestamp : null, lastExtrasTs);
|
|
71239
|
+
if (fadeInMs === null) return;
|
|
71240
|
+
extrasPart.current = {
|
|
70881
71241
|
detections: data.detections,
|
|
70882
71242
|
frameWidth: data.frameWidth,
|
|
70883
71243
|
frameHeight: data.frameHeight
|
|
70884
|
-
}
|
|
70885
|
-
|
|
70886
|
-
|
|
71244
|
+
};
|
|
71245
|
+
publish();
|
|
71246
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
71247
|
+
extrasTimer.current = setTimeout(() => {
|
|
71248
|
+
extrasPart.current = null;
|
|
71249
|
+
publish();
|
|
71250
|
+
}, fadeInMs);
|
|
70887
71251
|
});
|
|
70888
71252
|
return () => {
|
|
70889
71253
|
sub?.unsubscribe();
|
|
70890
|
-
if (
|
|
71254
|
+
if (extrasTimer.current) clearTimeout(extrasTimer.current);
|
|
70891
71255
|
};
|
|
70892
|
-
}, [
|
|
71256
|
+
}, [
|
|
71257
|
+
trpc,
|
|
71258
|
+
deviceId,
|
|
71259
|
+
admit,
|
|
71260
|
+
publish
|
|
71261
|
+
]);
|
|
70893
71262
|
(0, react$1.useEffect)(() => {
|
|
70894
71263
|
if (deviceId === null) return;
|
|
70895
71264
|
const sub = safeSubscribe(trpc, "detection.result", (event) => {
|
|
70896
71265
|
const e = event;
|
|
70897
71266
|
if (!matchesDevice(e?.source, deviceId)) return;
|
|
70898
|
-
if (Date.now() - lastEnrichedAt.current < DETECTION_FADE_MS) return;
|
|
70899
71267
|
const frame = e.data?.frame;
|
|
70900
71268
|
if (!frame?.detections?.length) return;
|
|
70901
|
-
|
|
71269
|
+
const fadeInMs = admit(typeof frame.timestamp === "number" ? frame.timestamp : null, lastRawTs);
|
|
71270
|
+
if (fadeInMs === null) return;
|
|
71271
|
+
rawPart.current = {
|
|
70902
71272
|
detections: frame.detections,
|
|
70903
71273
|
frameWidth: frame.width,
|
|
70904
71274
|
frameHeight: frame.height
|
|
70905
|
-
}
|
|
70906
|
-
|
|
70907
|
-
|
|
71275
|
+
};
|
|
71276
|
+
publish();
|
|
71277
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
71278
|
+
rawTimer.current = setTimeout(() => {
|
|
71279
|
+
rawPart.current = null;
|
|
71280
|
+
publish();
|
|
71281
|
+
}, fadeInMs);
|
|
70908
71282
|
});
|
|
70909
71283
|
return () => {
|
|
70910
71284
|
sub?.unsubscribe();
|
|
70911
|
-
if (
|
|
71285
|
+
if (rawTimer.current) clearTimeout(rawTimer.current);
|
|
70912
71286
|
};
|
|
70913
|
-
}, [
|
|
71287
|
+
}, [
|
|
71288
|
+
trpc,
|
|
71289
|
+
deviceId,
|
|
71290
|
+
admit,
|
|
71291
|
+
publish
|
|
71292
|
+
]);
|
|
70914
71293
|
(0, react$1.useEffect)(() => {
|
|
70915
71294
|
if (deviceId === null) return;
|
|
70916
71295
|
const sub = safeSubscribe(trpc, "detection.phase-transition", (event) => {
|
|
@@ -71503,6 +71882,7 @@ exports.DATA_RECORDS_OWNER_PARAM = DATA_RECORDS_OWNER_PARAM;
|
|
|
71503
71882
|
exports.DATA_RECORD_FACET_GROUPS = DATA_RECORD_FACET_GROUPS;
|
|
71504
71883
|
exports.DEFAULT_COLOR = DEFAULT_COLOR;
|
|
71505
71884
|
exports.DEFAULT_PAGINATION_LABELS = DEFAULT_PAGINATION_LABELS;
|
|
71885
|
+
exports.DEFAULT_UNIT_PREFERENCES = DEFAULT_UNIT_PREFERENCES;
|
|
71506
71886
|
exports.DEVICE_COLUMNS = DEVICE_COLUMNS;
|
|
71507
71887
|
exports.DEVICE_EXPORT_PAGE_SIZE_KEY = DEVICE_EXPORT_PAGE_SIZE_KEY;
|
|
71508
71888
|
exports.DEVICE_LIST_PAGE_SIZE_KEY = DEVICE_LIST_PAGE_SIZE_KEY;
|
|
@@ -71914,6 +72294,7 @@ exports.SwitchInlineControl = SwitchInlineControl;
|
|
|
71914
72294
|
exports.SwitchPanel = SwitchPanel;
|
|
71915
72295
|
exports.SystemProvider = SystemProvider;
|
|
71916
72296
|
exports.TABLE_SCROLL_MAX_H = TABLE_SCROLL_MAX_H;
|
|
72297
|
+
exports.TEMPERATURE_SCALES = TEMPERATURE_SCALES;
|
|
71917
72298
|
exports.TEXT_FIELD_LABEL = TEXT_FIELD_LABEL;
|
|
71918
72299
|
exports.TEXT_HINT = TEXT_HINT;
|
|
71919
72300
|
exports.TEXT_METRIC = TEXT_METRIC;
|
|
@@ -71949,10 +72330,13 @@ exports.TrendingUp = TrendingUp;
|
|
|
71949
72330
|
exports.TriangleAlert = TriangleAlert;
|
|
71950
72331
|
exports.Truck = Truck;
|
|
71951
72332
|
exports.Type = Type;
|
|
72333
|
+
exports.UNIT_PREFERENCES_NAMESPACE = UNIT_PREFERENCES_NAMESPACE;
|
|
71952
72334
|
exports.UiTextProvider = UiTextProvider;
|
|
71953
72335
|
exports.UiTrans = UiTrans;
|
|
71954
72336
|
exports.Undo2 = Undo2;
|
|
71955
72337
|
exports.UnitPicker = UnitPicker;
|
|
72338
|
+
exports.UnitPreferencesFromServer = UnitPreferencesFromServer;
|
|
72339
|
+
exports.UnitPreferencesProvider = UnitPreferencesProvider;
|
|
71956
72340
|
exports.Unlink2 = Unlink2;
|
|
71957
72341
|
exports.Upload = Upload;
|
|
71958
72342
|
exports.User = User;
|
|
@@ -72220,6 +72604,7 @@ exports.pickerSearchText = pickerSearchText;
|
|
|
72220
72604
|
exports.pickerZoneFilterSuspended = pickerZoneFilterSuspended;
|
|
72221
72605
|
exports.pinnedStreamChoices = pinnedStreamChoices;
|
|
72222
72606
|
exports.placePopover = placePopover;
|
|
72607
|
+
exports.preferenceForQuantity = preferenceForQuantity;
|
|
72223
72608
|
exports.previousWindow = previousWindow;
|
|
72224
72609
|
Object.defineProperty(exports, "projectSubjectPath", {
|
|
72225
72610
|
enumerable: true,
|
|
@@ -72232,6 +72617,7 @@ exports.readHistory = readHistory;
|
|
|
72232
72617
|
exports.readHistoryPanels = readHistoryPanels;
|
|
72233
72618
|
exports.readItemId = readItemId;
|
|
72234
72619
|
exports.readStoredGroupBy = readStoredGroupBy;
|
|
72620
|
+
exports.readUnitPreferences = readUnitPreferences;
|
|
72235
72621
|
exports.reduceSessionRun = reduceSessionRun;
|
|
72236
72622
|
exports.registerAddonBundle = registerAddonBundle;
|
|
72237
72623
|
exports.renderMarkup = renderMarkup;
|
|
@@ -72328,6 +72714,7 @@ exports.typeLabel = typeLabel;
|
|
|
72328
72714
|
exports.unitAxes = unitAxes;
|
|
72329
72715
|
exports.useAccessoriesGetStatus = useAccessoriesGetStatus;
|
|
72330
72716
|
exports.useAccessoriesSetChildHidden = useAccessoriesSetChildHidden;
|
|
72717
|
+
exports.useAccountUnitPreferences = useAccountUnitPreferences;
|
|
72331
72718
|
exports.useActionSourceListActions = useActionSourceListActions;
|
|
72332
72719
|
exports.useActionSourceRunAction = useActionSourceRunAction;
|
|
72333
72720
|
exports.useActionSourceStopAction = useActionSourceStopAction;
|
|
@@ -73391,6 +73778,11 @@ exports.useRecordingOnboardGetStatus = useRecordingOnboardGetStatus;
|
|
|
73391
73778
|
exports.useRecordingOnboardSetSettings = useRecordingOnboardSetSettings;
|
|
73392
73779
|
exports.useRecordingSignalGetStatus = useRecordingSignalGetStatus;
|
|
73393
73780
|
exports.useRemoteComponent = useRemoteComponent;
|
|
73781
|
+
exports.useRemoteGetConfig = useRemoteGetConfig;
|
|
73782
|
+
exports.useRemoteListCommands = useRemoteListCommands;
|
|
73783
|
+
exports.useRemoteSendCommand = useRemoteSendCommand;
|
|
73784
|
+
exports.useRemoteSendMediaCommand = useRemoteSendMediaCommand;
|
|
73785
|
+
exports.useRemoteSetConfig = useRemoteSetConfig;
|
|
73394
73786
|
exports.useRoleDisplayDefaults = useRoleDisplayDefaults;
|
|
73395
73787
|
exports.useRunningStateGetStatus = useRunningStateGetStatus;
|
|
73396
73788
|
exports.useSceneMonitorCaptureReference = useSceneMonitorCaptureReference;
|
|
@@ -73543,6 +73935,11 @@ exports.useSystemSetRetentionConfig = useSystemSetRetentionConfig;
|
|
|
73543
73935
|
exports.useSystemSetSiteLocation = useSystemSetSiteLocation;
|
|
73544
73936
|
exports.useTamperGetStatus = useTamperGetStatus;
|
|
73545
73937
|
exports.useTemperatureSensorGetStatus = useTemperatureSensorGetStatus;
|
|
73938
|
+
exports.useTemplateCreate = useTemplateCreate;
|
|
73939
|
+
exports.useTemplateDelete = useTemplateDelete;
|
|
73940
|
+
exports.useTemplateGet = useTemplateGet;
|
|
73941
|
+
exports.useTemplateList = useTemplateList;
|
|
73942
|
+
exports.useTemplateUpdate = useTemplateUpdate;
|
|
73546
73943
|
exports.useTerminalSessionAdoptLegacyMonitor = useTerminalSessionAdoptLegacyMonitor;
|
|
73547
73944
|
exports.useTerminalSessionClose = useTerminalSessionClose;
|
|
73548
73945
|
exports.useTerminalSessionCreateInstance = useTerminalSessionCreateInstance;
|
|
@@ -73574,6 +73971,7 @@ exports.useTrainingStartJob = useTrainingStartJob;
|
|
|
73574
73971
|
exports.useTrainingStartJobBatch = useTrainingStartJobBatch;
|
|
73575
73972
|
exports.useTurnProviderGetTurnServers = useTurnProviderGetTurnServers;
|
|
73576
73973
|
exports.useUiText = useUiText;
|
|
73974
|
+
exports.useUnitPreferences = useUnitPreferences;
|
|
73577
73975
|
exports.useUpdateGetStatus = useUpdateGetStatus;
|
|
73578
73976
|
exports.useUpdateInstallUpdate = useUpdateInstallUpdate;
|
|
73579
73977
|
exports.useUrlQueryParam = useUrlQueryParam;
|