chore: Enforce a few house rules via eslint (#2539)
This commit is contained in:
committed by
dermotduffy
parent
5572ec728e
commit
921d45e577
@@ -9,13 +9,13 @@ export class EffectAction extends AdvancedCameraCardAction<EffectActionConfig> {
|
||||
const action = this._getAction();
|
||||
switch (action.effect_action) {
|
||||
case 'start':
|
||||
api.getEffectsManager().startEffect(action.effect);
|
||||
void api.getEffectsManager().startEffect(action.effect);
|
||||
break;
|
||||
case 'stop':
|
||||
api.getEffectsManager().stopEffect(action.effect);
|
||||
break;
|
||||
case 'toggle':
|
||||
api.getEffectsManager().toggleEffect(action.effect);
|
||||
void api.getEffectsManager().toggleEffect(action.effect);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
|
||||
|
||||
// If `enabled` is explicit, use it. If only `type` is being changed, leave
|
||||
// `enabled` untouched (undefined = no change). Otherwise (neither set),
|
||||
// toggle the current enabled value — this is the menu-button show/hide use
|
||||
// toggle the current enabled value -- this is the menu-button show/hide use
|
||||
// case.
|
||||
const enabled =
|
||||
action.enabled ??
|
||||
|
||||
@@ -28,9 +28,8 @@ export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfi
|
||||
return;
|
||||
}
|
||||
|
||||
(type === 'ptz'
|
||||
? this._toPTZAction(targetID)
|
||||
: this._toPTZDigitalAction(targetID)
|
||||
void (
|
||||
type === 'ptz' ? this._toPTZAction(targetID) : this._toPTZDigitalAction(targetID)
|
||||
).execute(api);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,6 @@ export class AutomationsManager {
|
||||
--this._nestedAutomationExecutions;
|
||||
}
|
||||
};
|
||||
runActions(automation.actions);
|
||||
void runActions(automation.actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ export class CallManager {
|
||||
restoreView &&
|
||||
(previousView.view !== 'live' || previousView.camera !== call.cameraID)
|
||||
) {
|
||||
viewManager.setViewByParametersWithExistingQuery({
|
||||
void viewManager.setViewByParametersWithExistingQuery({
|
||||
baseView: previousView,
|
||||
force: true,
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ export abstract class GeneratedTone implements Tone {
|
||||
// a stopped tone can never re-arm its loop.
|
||||
protected _scheduleNext(intervalSeconds: number): void {
|
||||
/* istanbul ignore next: defensive guard against a subclass calling
|
||||
_scheduleNext after stop() — JS single-threading makes this unreachable
|
||||
_scheduleNext after stop() -- JS single-threading makes this unreachable
|
||||
from the existing subclasses -- @preserve */
|
||||
if (!this._context) {
|
||||
return;
|
||||
|
||||
@@ -79,8 +79,8 @@ export class CardElementManager {
|
||||
// These initializers are called when the config is updated, but on initial
|
||||
// creation of the card hass is not yet available when the config is first
|
||||
// loaded.
|
||||
this._api.getDefaultManager().initialize();
|
||||
this._api.getMediaPlayerManager().initialize();
|
||||
void this._api.getDefaultManager().initialize();
|
||||
void this._api.getMediaPlayerManager().initialize();
|
||||
|
||||
this._api
|
||||
.getHASSManager()
|
||||
@@ -184,7 +184,7 @@ export class CardElementManager {
|
||||
this._api.getFullscreenManager().disconnect();
|
||||
this._api.getPIPManager().uninitialize();
|
||||
this._api.getKeyboardStateManager().uninitialize();
|
||||
this._api.getActionsManager().uninitialize();
|
||||
void this._api.getActionsManager().uninitialize();
|
||||
this._api.getInteractionManager().uninitialize();
|
||||
this._api.getDefaultManager().uninitialize();
|
||||
this._api.getHASSManager().getStateWatcher()?.unsubscribe(this.update);
|
||||
@@ -200,7 +200,7 @@ export class CardElementManager {
|
||||
this._api
|
||||
.getInitializationManager()
|
||||
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
|
||||
this._api.getCameraManager().destroy();
|
||||
void this._api.getCameraManager().destroy();
|
||||
|
||||
this._element.removeEventListener(
|
||||
'mousemove',
|
||||
|
||||
@@ -154,7 +154,7 @@ export class ConfigManager {
|
||||
// automations, which destroys associated ConditionsManagers. If a condition
|
||||
// transition (e.g. microphone connect) triggers both a user automation and
|
||||
// an unrelated override, an unconditional reload would delete the
|
||||
// automation mid-transition — the freshly created replacement has no prior
|
||||
// automation mid-transition -- the freshly created replacement has no prior
|
||||
// state, treats the current condition as its baseline, and never fires the
|
||||
// action.
|
||||
const runIfChanged = <T>(
|
||||
@@ -189,7 +189,7 @@ export class ConfigManager {
|
||||
(config) => [config.cameras, config.cameras_global],
|
||||
() => {
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||
this._api.getCameraManager().destroy();
|
||||
void this._api.getCameraManager().destroy();
|
||||
},
|
||||
true,
|
||||
);
|
||||
@@ -203,7 +203,7 @@ export class ConfigManager {
|
||||
true,
|
||||
);
|
||||
|
||||
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
|
||||
void this._initializeBackgroundAndUpdate(previousConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,7 +92,7 @@ export class OverridesManager {
|
||||
|
||||
const parseResult = advancedCameraCardConfigSchema.safeParse(output);
|
||||
if (!parseResult.success) {
|
||||
// Surface one co-located failure object per Zod issue — path, the value
|
||||
// Surface one co-located failure object per Zod issue -- path, the value
|
||||
// the user actually wrote, and the most informative "expected" field for
|
||||
// this issue code. Avoids dumping the full merged config (which is
|
||||
// mostly schema defaults the user never wrote) and keeps the reader from
|
||||
|
||||
@@ -166,7 +166,7 @@ export class EffectsManager implements EffectsManagerInterface {
|
||||
private _startPendingEffects(): void {
|
||||
for (const [name, options] of this._pendingEffects.entries()) {
|
||||
this._pendingEffects.delete(name);
|
||||
this._startEffect(name, options);
|
||||
void this._startEffect(name, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,12 @@ export class ScreenfullFullScreenProvider
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
screenfull.request(this._api.getCardElementManager().getElement());
|
||||
// A denied request (or an exit when not in fullscreen) leaves the UI
|
||||
// consistent: the 'change' handler only fires on a real transition.
|
||||
// Nothing to act on.
|
||||
screenfull.request(this._api.getCardElementManager().getElement()).catch(() => {});
|
||||
} else {
|
||||
screenfull.exit();
|
||||
screenfull.exit().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,10 @@ export class WebkitFullScreenProvider
|
||||
// clicking the 'X' which then fires this event). That's probably the rare
|
||||
// case though.
|
||||
this._playTimer.start(WEBKIT_PLAY_SECONDS, () => {
|
||||
this._getVideoElement()?.play();
|
||||
// Best-effort resume after a fullscreen exit.
|
||||
this._getVideoElement()
|
||||
?.play()
|
||||
.catch(() => {});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class HASSManager implements HASSManagerReadonlyInterface {
|
||||
);
|
||||
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||
this._api.getCameraManager().destroy();
|
||||
void this._api.getCameraManager().destroy();
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
|
||||
this._api
|
||||
.getInitializationManager()
|
||||
|
||||
@@ -80,7 +80,7 @@ export class InitializationManager {
|
||||
if (!this._shouldInitializeMandatory()) {
|
||||
return;
|
||||
}
|
||||
/* async */ this.initializeMandatory();
|
||||
void this.initializeMandatory();
|
||||
}
|
||||
|
||||
private _shouldInitializeMandatory(): boolean {
|
||||
|
||||
@@ -29,7 +29,7 @@ export class IssueManager {
|
||||
private _suspended = false;
|
||||
|
||||
// Reentrancy guard: evaluate() calls setState() on the condition state
|
||||
// manager, which fires listeners synchronously — including the one
|
||||
// manager, which fires listeners synchronously -- including the one
|
||||
// registered in this constructor. Without this guard, detectDynamic()
|
||||
// and presence computation would run twice per evaluation.
|
||||
private _evaluating = false;
|
||||
@@ -165,16 +165,14 @@ export class IssueManager {
|
||||
// normal re-evaluation (on any condition-state change).
|
||||
//
|
||||
// `initialized: true` in the change payload means mandatory initialization
|
||||
// just finished — see InitializationManager._initializeMandatory. That's
|
||||
// just finished -- see InitializationManager._initializeMandatory. That's
|
||||
// also the earliest point at which the full HASS object is guaranteed
|
||||
// ready for websocket calls (e.g. LegacyResourceIssue's lovelace/resources
|
||||
// fetch). Because `initialized` is latched (its comment notes it never
|
||||
// changes again), this block fires exactly once per IssueManager life.
|
||||
private _onStateChange(change: ConditionStateChange): void {
|
||||
if (change.change.initialized === true && change.new.hass) {
|
||||
/* async */ this._stateManager
|
||||
.detectStatic(change.new.hass)
|
||||
.then(() => this.evaluate());
|
||||
void this._stateManager.detectStatic(change.new.hass).then(() => this.evaluate());
|
||||
}
|
||||
this.evaluate();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ConnectionIssue implements Issue {
|
||||
private _state: ConnectionState = 'ready';
|
||||
|
||||
public detectDynamic(state: ConditionState): void {
|
||||
// Before HASS is ever provided, leave state untouched — undefined hass is
|
||||
// Before HASS is ever provided, leave state untouched -- undefined hass is
|
||||
// not a disconnection, just "not yet initialized".
|
||||
if (state.hass === undefined) {
|
||||
return;
|
||||
|
||||
@@ -44,7 +44,7 @@ export class InitializationIssue extends AbstractErrorIssue {
|
||||
// resources (WebSocket subscriptions, listeners) before the CAMERAS
|
||||
// init aspect replaces the instance via createCameraManager().
|
||||
this._api.getInitializationManager().uninitializeMandatory();
|
||||
this._api.getCameraManager().destroy();
|
||||
void this._api.getCameraManager().destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class MediaLoadIssue implements Issue {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Explicit trigger — called when a component fires an issue:trigger event.
|
||||
// Explicit trigger -- called when a component fires an issue:trigger event.
|
||||
// =========================================================================
|
||||
|
||||
public trigger(context: IssueTriggerContext['media_load']): void {
|
||||
@@ -45,7 +45,7 @@ export class MediaLoadIssue implements Issue {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Detection — called by the manager on every state change.
|
||||
// Detection -- called by the manager on every state change.
|
||||
// =========================================================================
|
||||
|
||||
public detectDynamic(state: ConditionState): void {
|
||||
@@ -62,7 +62,7 @@ export class MediaLoadIssue implements Issue {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// State queries — called by the manager to read current state.
|
||||
// State queries -- called by the manager to read current state.
|
||||
// =========================================================================
|
||||
|
||||
public hasIssue(): boolean {
|
||||
@@ -113,7 +113,7 @@ export class MediaLoadIssue implements Issue {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Retry — called by the manager to schedule a media reload.
|
||||
// Retry -- called by the manager to schedule a media reload.
|
||||
// =========================================================================
|
||||
|
||||
public needsRetry(): boolean {
|
||||
@@ -144,7 +144,7 @@ export class MediaLoadIssue implements Issue {
|
||||
// re-attempts loading underneath. If the retry succeeds,
|
||||
// _handleMediaLoaded will clear everything when media:loaded fires. If
|
||||
// it fails silently (e.g. bogus stream name), the error stays visible
|
||||
// immediately — no new 10s grace period.
|
||||
// immediately -- no new 10s grace period.
|
||||
this._api.getViewManager().setViewWithMergedContext({ mediaEpoch });
|
||||
return false;
|
||||
}
|
||||
@@ -212,7 +212,7 @@ export class MediaLoadIssue implements Issue {
|
||||
this._timerTargetID = targetID;
|
||||
this._timer.start(MEDIA_LOADING_TIMEOUT_SECONDS, () => {
|
||||
// Record the error on timeout so retry() knows which epoch to bump.
|
||||
// targetID is guaranteed non-null here — the null case bails at the
|
||||
// targetID is guaranteed non-null here -- the null case bails at the
|
||||
// top of _handleMediaNotLoaded.
|
||||
this._erroredTargetIDs.add(targetID);
|
||||
this._activate();
|
||||
|
||||
@@ -31,7 +31,7 @@ export class MediaQueryIssue extends AbstractErrorIssue {
|
||||
return false;
|
||||
}
|
||||
this._error = null;
|
||||
this._api.getViewManager().setViewByParametersWithNewQuery();
|
||||
void this._api.getViewManager().setViewByParametersWithNewQuery();
|
||||
|
||||
// Exclusive retry. No other issue should attempt to retry until the next
|
||||
// evaluation cycle, when we'll know if this was successful.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { summarizeNotification } from '../../components-lib/notification/summari
|
||||
import { ConditionState } from '../../condition-trigger/conditions/types';
|
||||
import { Notification } from '../../config/schema/actions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { isTruthy } from '../../utils/basic';
|
||||
import { errorToConsole, isTruthy } from '../../utils/basic';
|
||||
import {
|
||||
Issue,
|
||||
IssueDescription,
|
||||
@@ -27,12 +27,18 @@ export class IssueStateManager implements IssueReadOnlyState {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Detection — static (one-shot on init) and dynamic (on every state change).
|
||||
// Detection -- static (one-shot on init) and dynamic (on every state change).
|
||||
// =========================================================================
|
||||
|
||||
public async detectStatic(hass: HomeAssistant): Promise<void> {
|
||||
for (const issue of this._issues.values()) {
|
||||
await issue.detectStatic?.(hass);
|
||||
try {
|
||||
await issue.detectStatic?.(hass);
|
||||
} catch (e) {
|
||||
// Isolate one issue's detection failure so it cannot abort detection
|
||||
// for the rest; log so the cause is visible.
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
this._logIfNew(issue);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +63,7 @@ export class IssueStateManager implements IssueReadOnlyState {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Queries — read active issue state.
|
||||
// Queries -- read active issue state.
|
||||
// =========================================================================
|
||||
|
||||
public getFullCardIssue(): IssueDescription | null {
|
||||
|
||||
@@ -28,11 +28,11 @@ export interface KeyedIssueDescription {
|
||||
}
|
||||
|
||||
// Map of currently active issues keyed by IssueKey, with each entry's value
|
||||
// being the issue's current rendered description. Stored as a Map (not just
|
||||
// a Set of keys) so that sub-state changes within an issue — e.g.
|
||||
// ConnectionIssue swapping between 'lost' and 'starting' — are reflected as
|
||||
// real value-level diffs to the condition state, triggering re-renders and
|
||||
// any user-defined conditions that depend on issue state.
|
||||
// being the issue's current rendered description. Stored as a Map (not just a
|
||||
// Set of keys) so that sub-state changes within an issue -- e.g.
|
||||
// ConnectionIssue swapping between 'lost' and 'starting' -- are reflected as
|
||||
// real value-level diffs to the condition state, triggering re-renders and any
|
||||
// user-defined conditions that depend on issue state.
|
||||
export type IssuePresence = Map<IssueKey, IssueDescription>;
|
||||
export interface IssueReadOnlyState {
|
||||
hasFullCardIssue(): boolean;
|
||||
@@ -80,7 +80,7 @@ export interface Issue {
|
||||
// loop (exclusive), false to allow subsequent issues to also retry.
|
||||
retry?(): boolean;
|
||||
|
||||
// Optional user-initiated fix. Not called by the issue infrastructure —
|
||||
// Optional user-initiated fix. Not called by the issue infrastructure --
|
||||
// callers (e.g. notification control actions) invoke this directly.
|
||||
fix?(hass: HomeAssistant): Promise<boolean>;
|
||||
|
||||
@@ -92,11 +92,11 @@ export interface Issue {
|
||||
|
||||
// Called when the card is detached. Issues with age-based timers (e.g.
|
||||
// loading-timeout timers) must stop them here so that time spent offscreen
|
||||
// doesn't count against the user. Must preserve already-active issue state
|
||||
// — a full-card issue visible at detach should still be visible on
|
||||
// reattach. No `resume` hook: IssueManager.resume() triggers a normal
|
||||
// evaluate(), so any timer that should restart is re-armed via
|
||||
// detectDynamic against the current condition state.
|
||||
// doesn't count against the user. Must preserve already-active issue state --
|
||||
// a full-card issue visible at detach should still be visible on reattach. No
|
||||
// `resume` hook: IssueManager.resume() triggers a normal evaluate(), so any
|
||||
// timer that should restart is re-armed via detectDynamic against the current
|
||||
// condition state.
|
||||
suspend?(): void;
|
||||
|
||||
// Release external resources (e.g. a listener registered on another manager)
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { LockPolicy } from './types';
|
||||
// - Stream-stopping / re-init actions: pause, reload, and casting (which
|
||||
// rehosts the stream to a media player).
|
||||
//
|
||||
// `call_start` is intentionally absent — it's the entry into the lock.
|
||||
// `call_end` is also absent — it dispatches via `setViewByParameters({ force:
|
||||
// `call_start` is intentionally absent -- it's the entry into the lock.
|
||||
// `call_end` is also absent -- it dispatches via `setViewByParameters({ force:
|
||||
// true })` to bypass the lock, so listing it here would be redundant.
|
||||
const CALL_DISRUPTIVE_ACTIONS: ReadonlySet<string> = new Set([
|
||||
// View / camera / substream changes.
|
||||
|
||||
@@ -27,7 +27,7 @@ export class MediaLoadedInfoManager {
|
||||
// never cleared by `clear` / `_clearTarget`, only by `initialize`.
|
||||
private _lastKnown: Map<string, MediaLoadedInfo> = new Map();
|
||||
|
||||
// The currently "active" target — the one whose info drives condition state
|
||||
// The currently "active" target -- the one whose info drives condition state
|
||||
// and card-level side effects. Driven by ViewManager on every view change.
|
||||
private _selected: string | null = null;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ interface QueryStringViewIntent {
|
||||
|
||||
// The substream change to apply alongside the view. Tri-state:
|
||||
// - `undefined`: no substream URL action present, no modifier issued.
|
||||
// - `string`: `substream_on=X` — engage stream X.
|
||||
// - `null`: `substream_off` — explicitly clear the override.
|
||||
// - `string`: `substream_on=X` -- engage stream X.
|
||||
// - `null`: `substream_off` -- explicitly clear the override.
|
||||
stream?: string | null;
|
||||
};
|
||||
other?: AdvancedCameraCardCustomActionConfig[];
|
||||
|
||||
@@ -126,7 +126,7 @@ export class ViewManager implements ViewManagerInterface {
|
||||
...options,
|
||||
});
|
||||
// A non-throwing factory call clears any prior view_incompatible /
|
||||
// media_query state — ensures a previously-dismissed mid-session popup
|
||||
// media_query state -- ensures a previously-dismissed mid-session popup
|
||||
// does not linger invisibly and re-pop on the next evaluation cycle,
|
||||
// and that a stale media_query failure from an abandoned gallery /
|
||||
// viewer doesn't follow the user into an unrelated view.
|
||||
@@ -361,7 +361,7 @@ export class ViewManager implements ViewManagerInterface {
|
||||
if (!this._api.getQueryStringManager().hasViewRelatedActionsToRun()) {
|
||||
// This is not awaited to allow the initialization to complete before the
|
||||
// query is answered.
|
||||
this.setViewDefaultWithNewQuery({ failSafe: true });
|
||||
void this.setViewDefaultWithNewQuery({ failSafe: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -329,7 +329,7 @@ class AdvancedCameraCard extends LitElement {
|
||||
|
||||
protected updated(): void {
|
||||
if (this._controller.getInitializationManager().isInitializedMandatory()) {
|
||||
this._controller.getQueryStringManager().executeIfNecessary();
|
||||
void this._controller.getQueryStringManager().executeIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export class MicrophoneActionsController {
|
||||
}
|
||||
this._callAnswered = answered;
|
||||
if (answered) {
|
||||
this._unmuteIfConfigured('call');
|
||||
void this._unmuteIfConfigured('call');
|
||||
} else {
|
||||
this._muteIfConfigured('call');
|
||||
}
|
||||
@@ -106,7 +106,10 @@ export class MicrophoneActionsController {
|
||||
this._options?.microphoneManager &&
|
||||
this._options.autoUnmuteConditions?.includes(condition)
|
||||
) {
|
||||
await this._options.microphoneManager.unmute();
|
||||
// A denied or missing microphone already shows in the UI: the menu
|
||||
// microphone button switches to its forbidden icon. A failed auto-unmute
|
||||
// has nothing more to act on.
|
||||
await this._options.microphoneManager.unmute().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export class MediaActionsController {
|
||||
public setMicrophoneState(state: MicrophoneState): void {
|
||||
const previous = this._microphoneState;
|
||||
this._microphoneState = state;
|
||||
this._microphoneStateChangeHandler(previous, state);
|
||||
void this._microphoneStateChangeHandler(previous, state);
|
||||
}
|
||||
|
||||
// Audio-out auto-mute/unmute driven by call answer: unmute when the call
|
||||
@@ -85,10 +85,10 @@ export class MediaActionsController {
|
||||
this._callAnswered = answered;
|
||||
if (answered) {
|
||||
this._pendingCallStartAction = true;
|
||||
this._applyPendingCallStartAction();
|
||||
void this._applyPendingCallStartAction();
|
||||
} else {
|
||||
this._pendingCallStartAction = false;
|
||||
this._muteTargetIfConfigured('call');
|
||||
void this._muteTargetIfConfigured('call');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ export class MediaFilterController {
|
||||
const queryCameraIDs = query.getAllCameraIDs();
|
||||
const cameraID = queryCameraIDs.size === 1 ? [...queryCameraIDs][0] : undefined;
|
||||
|
||||
this._viewManager?.setViewByParametersWithExistingQuery({
|
||||
void this._viewManager?.setViewByParametersWithExistingQuery({
|
||||
params: {
|
||||
query,
|
||||
// If single camera, set it as the active camera for menu navigation
|
||||
|
||||
@@ -9,7 +9,7 @@ interface MediaLoadedInfoSinkConfig {
|
||||
// notification.
|
||||
getTargetID: () => string | null;
|
||||
|
||||
// Fires when the active info changes — i.e. when the active target's entry
|
||||
// Fires when the active info changes -- i.e. when the active target's entry
|
||||
// transitions (load arrives, abort, or selection changes the active entry).
|
||||
// Loads for other targets are cached but do not fire the callback.
|
||||
callback?: (info: MediaLoadedInfo | null) => void;
|
||||
@@ -28,7 +28,7 @@ interface MediaLoadedInfoSinkConfig {
|
||||
* user selects a slide whose media has already loaded, the sink immediately
|
||||
* exposes the cached entry.
|
||||
*
|
||||
* Lifecycle asymmetry — `callback` fires on:
|
||||
* Lifecycle asymmetry -- `callback` fires on:
|
||||
* - the active target's load arrival,
|
||||
* - the active target's source aborting (with `null`), and
|
||||
* - selection changing to / from a target whose active info differs.
|
||||
@@ -46,7 +46,7 @@ export class MediaLoadedInfoSinkController implements ReactiveController {
|
||||
// stale abort can't blow away an entry that's since been overwritten.
|
||||
private _byTarget = new Map<string, MediaLoadedInfo>();
|
||||
|
||||
// The targetID whose info we last surfaced — drives `hostUpdated` change
|
||||
// The targetID whose info we last surfaced -- drives `hostUpdated` change
|
||||
// detection. `_lastActiveInfo` records what the callback last saw, so we
|
||||
// don't fire it for no-op selection changes (e.g. selection changes but
|
||||
// both old and new are null/loaded with the same info reference).
|
||||
@@ -67,7 +67,7 @@ export class MediaLoadedInfoSinkController implements ReactiveController {
|
||||
}
|
||||
|
||||
public hostUpdated(): void {
|
||||
// Detect selection changes — `getTargetID` is owned by the host and may
|
||||
// Detect selection changes -- `getTargetID` is owned by the host and may
|
||||
// flip when its props change (carousel slide change, view change, etc.).
|
||||
const newID = this._config.getTargetID();
|
||||
if (newID !== this._lastActiveTargetID) {
|
||||
|
||||
@@ -53,7 +53,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController {
|
||||
private _abort: AbortController | null = null;
|
||||
|
||||
// Survives disconnect so we can re-dispatch on reconnect. Only ever holds
|
||||
// info validated by `set` — i.e., always has a targetID.
|
||||
// info validated by `set` -- i.e., always has a targetID.
|
||||
private _lastSet: TargetedMediaLoadedInfo | null = null;
|
||||
|
||||
constructor(
|
||||
@@ -67,7 +67,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController {
|
||||
|
||||
public hostConnected(): void {
|
||||
// Two early-returns:
|
||||
// - `!_lastSet`: nothing to replay — either the host has never seen a
|
||||
// - `!_lastSet`: nothing to replay -- either the host has never seen a
|
||||
// media load or the cache was discarded as stale on a prior reconnect
|
||||
// (see below).
|
||||
// - `_abort` non-null: a dispatch is already live, meaning we're already
|
||||
@@ -79,7 +79,7 @@ export class MediaLoadedInfoSourceController implements ReactiveController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Revalidate against the current targetID — the host's property may have
|
||||
// Revalidate against the current targetID -- the host's property may have
|
||||
// flipped while we were disconnected. Replaying the cached info under a
|
||||
// stale targetID would misregister with the manager.
|
||||
if (this._lastSet.targetID === this._config.getTargetID()) {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const navigateUp = (options?: FolderNavigationParamaters | null): void =>
|
||||
},
|
||||
);
|
||||
|
||||
options?.viewManagerEpoch.manager.setViewByParametersWithExistingQuery({
|
||||
void options?.viewManagerEpoch.manager.setViewByParametersWithExistingQuery({
|
||||
params: { query },
|
||||
});
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export const navigateToFolder = (
|
||||
},
|
||||
);
|
||||
|
||||
options?.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
|
||||
void options?.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
|
||||
params: { query },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export class PTZDragController implements ReactiveController {
|
||||
// continuous mode. Below that threshold, drag-end dispatches relative.
|
||||
private _continuous = false;
|
||||
|
||||
// When a pinch occurs mid-drag, the drag is "poisoned" — all remaining
|
||||
// When a pinch occurs mid-drag, the drag is "poisoned" -- all remaining
|
||||
// drag events for that gesture are ignored to prevent stray pan/tilt.
|
||||
private _dragCancelledByPinch = false;
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export class SignedURLController implements ReactiveController {
|
||||
|
||||
// When the endpoint requires signing or proxying, the URL must go through
|
||||
// the async resolution path. For proxied URLs, under no circumstances
|
||||
// should we fall back to returning the unproxied URL — doing so risks
|
||||
// should we fall back to returning the unproxied URL -- doing so risks
|
||||
// leaking traffic or causing mixed-content errors.
|
||||
if (options.proxyConfig?.enabled || options.endpoint?.sign) {
|
||||
return this._value;
|
||||
|
||||
@@ -408,7 +408,7 @@ export class TimelineController {
|
||||
this._setTargetBarAppropriately(targetTime);
|
||||
}
|
||||
|
||||
this._throttledSetViewDuringRangeChange(targetTime, properties);
|
||||
void this._throttledSetViewDuringRangeChange(targetTime, properties);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export class AdvancedCameraCardImageUpdatingPlayer
|
||||
() => dispatchMediaPlayEvent(this),
|
||||
() => dispatchMediaPauseEvent(this),
|
||||
// Clear image load errors on each timer tick so the next render retries
|
||||
// the <img> — but only for modes where the underlying URL genuinely
|
||||
// the <img> -- but only for modes where the underlying URL genuinely
|
||||
// changes between ticks (camera/entity snapshots). For mode: url, the
|
||||
// same static URL will fail the same way every time, so clearing the
|
||||
// error just causes a visible flicker (notification → blank <img> →
|
||||
|
||||
@@ -233,7 +233,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
|
||||
private _setViewCameraID(cameraID?: string | null): void {
|
||||
if (cameraID) {
|
||||
this.viewManagerEpoch?.manager.setViewByParametersWithNewQuery({
|
||||
void this.viewManagerEpoch?.manager.setViewByParametersWithNewQuery({
|
||||
params: {
|
||||
camera: cameraID,
|
||||
},
|
||||
@@ -319,9 +319,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
const controller = this._mediaLoadedInfoSinkController.get()?.mediaPlayerController;
|
||||
// Fire-and-forget; the `volumechange` event drives the re-render.
|
||||
if (controller?.isMuted()) {
|
||||
controller.unmute();
|
||||
void controller.unmute();
|
||||
} else {
|
||||
controller?.mute();
|
||||
void controller?.mute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
const selectedCameraIndex = this._getSelectedCameraIndex();
|
||||
|
||||
if (this.viewFilterCameraID) {
|
||||
this._mediaActionsController.setTarget(
|
||||
void this._mediaActionsController.setTarget(
|
||||
selectedCameraIndex,
|
||||
// Camera in this carousel is only selected if the camera from the
|
||||
// view matches the filtered camera.
|
||||
@@ -486,7 +486,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
);
|
||||
} else {
|
||||
// Carousel is not filtered, so the targeted camera is always selected.
|
||||
this._mediaActionsController.setTarget(selectedCameraIndex, true);
|
||||
void this._mediaActionsController.setTarget(selectedCameraIndex, true);
|
||||
}
|
||||
|
||||
this._mediaHeightController.setSelected(selectedCameraIndex);
|
||||
|
||||
@@ -103,7 +103,7 @@ export class AdvancedCameraCardLiveGrid extends LitElement {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('viewManagerEpoch') && this._needsGrid()) {
|
||||
import('../media-grid.js');
|
||||
void import('../media-grid.js');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export class AdvancedCameraCardLive extends LitElement {
|
||||
// from a hidden live view. Treat the live view as having no selected
|
||||
// camera unless it is the active view.
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
this._microphoneActionsController.setSelectedCamera(
|
||||
void this._microphoneActionsController.setSelectedCamera(
|
||||
view?.is('live') ? view.camera ?? null : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
||||
// VideoRTC owns the transition: it updates microphoneStream, swaps the
|
||||
// track on the pre-armed transceiver, and validates against stale async
|
||||
// completions before any reconnect fallback. Fire-and-forget is fine.
|
||||
/* async */ this._player.setMicrophoneStream(this.microphoneStream ?? null);
|
||||
void this._player.setMicrophoneStream(this.microphoneStream ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ export class VideoRTC extends HTMLElement {
|
||||
/**
|
||||
* Owns the microphone stream transition end-to-end: updates the property,
|
||||
* extracts the outbound audio track, and swaps it onto the pre-armed audio
|
||||
* transceiver via `replaceTrack` — no SDP renegotiation, no visible reload.
|
||||
* transceiver via `replaceTrack` -- no SDP renegotiation, no visible reload.
|
||||
*
|
||||
* Falls back to a full reconnect if `replaceTrack` rejects, but only when
|
||||
* the rejection still describes the current desired state. The transceiver
|
||||
@@ -410,8 +410,8 @@ export class VideoRTC extends HTMLElement {
|
||||
*/
|
||||
disconnectedCallback() {
|
||||
// Synchronous manager-side cleanup by aborting the load's signal. The
|
||||
// signal's abort listeners — registered by the card-root listener and
|
||||
// any sinks in the bubble path — fire even though `parentNode` is
|
||||
// signal's abort listeners -- registered by the card-root listener and
|
||||
// any sinks in the bubble path -- fire even though `parentNode` is
|
||||
// already null, because abort is plain JS, not DOM-event-bound.
|
||||
this._abortController?.abort();
|
||||
this._abortController = null;
|
||||
@@ -833,7 +833,7 @@ export class VideoRTC extends HTMLElement {
|
||||
// Always pre-arm a single outbound audio transceiver so the SDP advertises
|
||||
// the slot from the start. With the slot in place, the mic track can be
|
||||
// attached/detached later via `setMicrophoneStream` (replaceTrack) without
|
||||
// renegotiating — avoiding a visible reload of this cell each time grid
|
||||
// renegotiating -- avoiding a visible reload of this cell each time grid
|
||||
// selection moves the mic between cameras.
|
||||
//
|
||||
// Pure SDP allocation: the kind-only `addTransceiver('audio', ...)` form
|
||||
|
||||
@@ -200,7 +200,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
public updated(): void {
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
void this.updateComplete.then(() => {
|
||||
this._videoRTC = this.renderRoot?.querySelector('#webrtc') ?? null;
|
||||
const video = this._getVideo();
|
||||
if (video) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class AdvancedCameraCardLoading extends LitElement {
|
||||
}
|
||||
|
||||
private _startEffect(effect: EffectName): void {
|
||||
this.effectsManager?.startEffect(effect, { fadeIn: false });
|
||||
void this.effectsManager?.startEffect(effect, { fadeIn: false });
|
||||
this._effectName = effect;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class AdvancedCameraCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
this.cameraManager,
|
||||
this.foldersManager,
|
||||
);
|
||||
this._mediaFilterController.computeMetadataOptions(this.cameraManager);
|
||||
void this._mediaFilterController.computeMetadataOptions(this.cameraManager);
|
||||
}
|
||||
|
||||
// The first time the viewManager is set, compute the initial default selections.
|
||||
|
||||
@@ -87,7 +87,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
||||
}
|
||||
|
||||
if (!this._optionTitles) {
|
||||
this._refreshOptionTitles();
|
||||
void this._refreshOptionTitles();
|
||||
}
|
||||
|
||||
const entityID = this.submenuSelect.entity;
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') {
|
||||
import('./timeline-core.js');
|
||||
void import('./timeline-core.js');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export class AdvancedCameraCardThumbnailFeatureThumbnail extends LitElement {
|
||||
this._embedThumbnailTask?.status === TaskStatus.INITIAL &&
|
||||
entries.some((entry) => entry.isIntersecting)
|
||||
) {
|
||||
this._embedThumbnailTask?.run();
|
||||
void this._embedThumbnailTask?.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
this._controller.setView(this.viewManagerEpoch ?? null),
|
||||
);
|
||||
} else {
|
||||
this._controller.setView(this.viewManagerEpoch ?? null);
|
||||
void this._controller.setView(this.viewManagerEpoch ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
(this._selected !== null && this._media?.[this._selected]?.getID()) || null,
|
||||
callback: () => {
|
||||
this._mediaHeightController.recalculate();
|
||||
this._seekHandler();
|
||||
void this._seekHandler();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -388,7 +388,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
?.context?.mediaViewer?.seek?.getTime() !==
|
||||
this.viewManagerEpoch?.oldView?.context?.mediaViewer?.seek?.getTime()
|
||||
) {
|
||||
this._seekHandler();
|
||||
void this._seekHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,7 +397,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
if (!this._media?.length || this._selected === null) {
|
||||
this._mediaActionsController.unsetTarget();
|
||||
} else {
|
||||
this._mediaActionsController.setTarget(
|
||||
void this._mediaActionsController.setTarget(
|
||||
this._selected,
|
||||
// Camera in this carousel is only selected if the camera from the view
|
||||
// matches the filtered camera.
|
||||
@@ -438,16 +438,16 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
const seekTimeInMedia = selectedMedia.includesTime(seek);
|
||||
this.toggleAttribute('unseekable', !seekTimeInMedia);
|
||||
if (!seekTimeInMedia && !mediaPlayerController.isPaused()) {
|
||||
mediaPlayerController.pause();
|
||||
void mediaPlayerController.pause();
|
||||
} else if (seekTimeInMedia && mediaPlayerController.isPaused()) {
|
||||
mediaPlayerController.play();
|
||||
void mediaPlayerController.play();
|
||||
}
|
||||
|
||||
const seekTime =
|
||||
(await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
|
||||
|
||||
if (seekTime !== null) {
|
||||
mediaPlayerController.seek(seekTime);
|
||||
void mediaPlayerController.seek(seekTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export class AdvancedCameraCardViewerGrid extends LitElement {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('viewManagerEpoch') && this._needsGrid()) {
|
||||
import('../media-grid.js');
|
||||
void import('../media-grid.js');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,11 +157,11 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
changedProps.has('resolvedMediaCache') ||
|
||||
changedProps.has('hass')
|
||||
) {
|
||||
this._resolveURL();
|
||||
void this._resolveURL();
|
||||
}
|
||||
|
||||
if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
|
||||
import('../zoomer.js');
|
||||
void import('../zoomer.js');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
.targetID=${mediaID}
|
||||
@click=${() => {
|
||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||
this._switchToRelatedClipView();
|
||||
void this._switchToRelatedClipView();
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-image-player>`}
|
||||
|
||||
@@ -90,16 +90,16 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
if (changedProps.has('viewManagerEpoch') || changedProps.has('config')) {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
if (view?.is('live') || this._shouldLivePreload()) {
|
||||
import('./live/index.js');
|
||||
void import('./live/index.js');
|
||||
}
|
||||
if (view?.isGalleryView()) {
|
||||
import('./gallery/gallery.js');
|
||||
void import('./gallery/gallery.js');
|
||||
} else if (view?.isViewerView()) {
|
||||
import('./viewer/index.js');
|
||||
void import('./viewer/index.js');
|
||||
} else if (view?.is('image')) {
|
||||
import('./image.js');
|
||||
void import('./image.js');
|
||||
} else if (view?.is('timeline')) {
|
||||
import('./timeline.js');
|
||||
void import('./timeline.js');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ConditionState } from '../conditions/types';
|
||||
// Parses a Home Assistant time-period value (a condition/trigger `for:`) to
|
||||
// seconds, matching HA's `cv.time_period`:
|
||||
// - a number, or a bare numeric string, is a count of seconds;
|
||||
// - a colon string is `HH:MM` or `HH:MM:SS` — HA reads TWO parts as
|
||||
// - a colon string is `HH:MM` or `HH:MM:SS` -- HA reads TWO parts as
|
||||
// hours:minutes (not minutes:seconds);
|
||||
// - a `{days, hours, minutes, seconds, milliseconds}` dict (each field a
|
||||
// number or a numeric string, e.g. once a template field has been rendered).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// A value that may be a single string or a list of strings — common across HA
|
||||
// A value that may be a single string or a list of strings -- common across HA
|
||||
// condition/trigger fields (e.g. `state`, `to`, `entity_id`).
|
||||
export const stringOrArray = z.string().or(z.string().array());
|
||||
|
||||
+9
-5
@@ -278,9 +278,9 @@ import {
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
|
||||
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
|
||||
CONF_VIEW_TRIGGERS_EVENT_HOLD_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
|
||||
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
|
||||
CONF_VIEW_TRIGGERS_EVENT_HOLD_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_DELAY_SECONDS,
|
||||
CONF_VIEW_TRIGGERS_UNTRIGGER_FORCE_SECONDS,
|
||||
DOCS_URL,
|
||||
@@ -294,7 +294,7 @@ import { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
import editorStyle from './scss/editor.scss';
|
||||
import type { CapabilityKey } from './types.js';
|
||||
import { arrayMove, prettifyTitle } from './utils/basic.js';
|
||||
import { arrayMove, errorToConsole, prettifyTitle } from './utils/basic.js';
|
||||
import { getCameraID } from './utils/camera.js';
|
||||
import { fireAdvancedCameraCardEvent } from './utils/fire-advanced-camera-card-event.js';
|
||||
import { getFolderID } from './utils/folder.js';
|
||||
@@ -1255,9 +1255,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
|
||||
protected willUpdate(): void {
|
||||
if (!this._initialized) {
|
||||
sideLoadHomeAssistantElements().then(() => {
|
||||
this._initialized = true;
|
||||
});
|
||||
sideLoadHomeAssistantElements()
|
||||
.then(() => {
|
||||
this._initialized = true;
|
||||
})
|
||||
// A failure leaves the editor with degraded HA form elements and is
|
||||
// retried on the next update; log it so the cause is at least visible.
|
||||
.catch((e) => errorToConsole(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ export const createProxiedEndpointIfNecessary = async (
|
||||
}
|
||||
|
||||
if (proxyConfig.dynamic) {
|
||||
// Strip hash fragment — it's client-side only and not relevant for
|
||||
// Strip hash fragment -- it's client-side only and not relevant for
|
||||
// proxy pattern matching.
|
||||
const url = endpoint.endpoint.split(/#/)[0];
|
||||
await addDynamicProxyURL(hass, url, {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { onAbort } from '../utils/abort-signal.js';
|
||||
import './ha-hls-player.js';
|
||||
import './ha-web-rtc-player.js';
|
||||
|
||||
customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
void customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
// ========================================================================================
|
||||
// From:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/data/camera.ts
|
||||
@@ -56,7 +56,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
|
||||
// ha-camera-stream renders up to three inner players (MJPEG / HLS /
|
||||
// WebRTC), only one visible. Inner leaves all fire `media:loaded`
|
||||
// independently — we suppress those at this boundary (`stopPropagation` in
|
||||
// independently -- we suppress those at this boundary (`stopPropagation` in
|
||||
// `_captureInnerLoad`), cache the latest per type, and republish the
|
||||
// visible one's info via our own source controller in `updated()`.
|
||||
private _mediaLoadedInfoPerStream: Record<StreamType, MediaLoadedInfo> = {};
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '../utils/media-info.js';
|
||||
import { ConstructableLitElement } from './types.js';
|
||||
|
||||
customElements.whenDefined('ha-hls-player').then(() => {
|
||||
void customElements.whenDefined('ha-hls-player').then(() => {
|
||||
const HaHlsPlayer = customElements.get('ha-hls-player') as ConstructableLitElement;
|
||||
|
||||
@customElement('advanced-camera-card-ha-hls-player')
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from '../utils/media-info.js';
|
||||
import { ConstructableLitElement } from './types.js';
|
||||
|
||||
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||
void customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||
const HaWebRtcPlayer = customElements.get(
|
||||
'ha-web-rtc-player',
|
||||
) as ConstructableLitElement;
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Transmitting: breathe between a softer and a fuller glow — never fully fades,
|
||||
// Transmitting: breathe between a softer and a fuller glow -- never fully fades,
|
||||
// so the camera always reads as live.
|
||||
@keyframes transmitting-vignette-pulse {
|
||||
0%,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Shared "pop" enter/exit animation for overlays.
|
||||
//
|
||||
// `@include pop-in` auto-plays an entrance on render. `@include pop-out` —
|
||||
// typically guarded by an `.exiting` class — plays the matching exit; it is
|
||||
// `@include pop-in` auto-plays an entrance on render. `@include pop-out` --
|
||||
// typically guarded by an `.exiting` class -- plays the matching exit; it is
|
||||
// named `pop-out` so an `animationend` handler can detect exit completion and
|
||||
// unmount the element.
|
||||
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export interface MediaLoadedInfo {
|
||||
mediaPlayerController?: MediaPlayerController;
|
||||
capabilities?: MediaLoadedCapabilities;
|
||||
|
||||
// Universal key identifying "what this media belongs to" — a camera ID for
|
||||
// Universal key identifying "what this media belongs to" -- a camera ID for
|
||||
// live, a media ID for the viewer, or a sentinel for the image view.
|
||||
targetID?: string;
|
||||
}
|
||||
|
||||
+7
-7
@@ -35,13 +35,13 @@ export const hasAudio = (
|
||||
pc?: RTCPeerConnection | null,
|
||||
mseCodecs?: string,
|
||||
): boolean => {
|
||||
// For WebRTC: Check if there's an audio receiver with an active track.
|
||||
// We check that the track is not muted because muted means no media data
|
||||
// is flowing (e.g., the source isn't producing audio). It is not related to
|
||||
// the audio being muted by the user on the receiving end.
|
||||
// Only trust receivers when the connection is actually established — a stale
|
||||
// RTCPeerConnection (e.g. WebRTC failed, fell back to MSE) will have
|
||||
// receivers with muted tracks that don't reflect actual media availability.
|
||||
// For WebRTC: Check if there's an audio receiver with an active track. We
|
||||
// check that the track is not muted because muted means no media data is
|
||||
// flowing (e.g., the source isn't producing audio). It is not related to the
|
||||
// audio being muted by the user on the receiving end. Only trust receivers
|
||||
// when the connection is actually established -- a stale RTCPeerConnection
|
||||
// (e.g. WebRTC failed, fell back to MSE) will have receivers with muted
|
||||
// tracks that don't reflect actual media availability.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2417
|
||||
if (pc && pc.connectionState === 'connected') {
|
||||
const receivers = pc.getReceivers();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AdvancedCameraCardError } from '../types.js';
|
||||
|
||||
// Narrows an unknown error to its structured object `context` — non-null and
|
||||
// of object type — or null if the error is not an AdvancedCameraCardError or
|
||||
// Narrows an unknown error to its structured object `context` -- non-null and
|
||||
// of object type -- or null if the error is not an AdvancedCameraCardError or
|
||||
// has no usable context. Consolidates the instanceof + typeof + null-guard
|
||||
// dance that notification builders and error handlers would otherwise repeat.
|
||||
export const getContextFromError = (error: unknown): object | null =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Walk up `element`'s ancestor chain (through shadow boundaries) looking for
|
||||
* an ancestor with the given tag name. Returns true if one is found and that
|
||||
* same element also appears in the event's composedPath — indicating the event
|
||||
* same element also appears in the event's composedPath -- indicating the event
|
||||
* originated from within the same subtree as the element.
|
||||
*/
|
||||
export const isAncestorInEventPath = (
|
||||
|
||||
@@ -11,10 +11,10 @@ const MEDIA_INFO_HEIGHT_CUTOFF = 50;
|
||||
const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF;
|
||||
|
||||
/**
|
||||
* Create a MediaLoadedInfo object. `targetID` is intentionally NOT an option
|
||||
* — it's owned by the source controller (`MediaLoadedInfoSourceController`)
|
||||
* and injected at dispatch time, so leaves don't have to (and can't) plumb
|
||||
* it through info construction.
|
||||
* Create a MediaLoadedInfo object. `targetID` is intentionally NOT an option --
|
||||
* it's owned by the source controller (`MediaLoadedInfoSourceController`) and
|
||||
* injected at dispatch time, so leaves don't have to (and can't) plumb it
|
||||
* through info construction.
|
||||
* @param source An event or HTMLElement that should be used as a source.
|
||||
* @returns A new info or null if one could not be created.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Usage of this function needs to be justified with a comment.
|
||||
export const sleep = async (seconds: number) => {
|
||||
// This is the low-level delay primitive callers reach for instead of a raw timer.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await new Promise((r) => setTimeout(r, seconds * 1000));
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ export class Timer {
|
||||
|
||||
public start(seconds: number, func: () => void): void {
|
||||
this.stop();
|
||||
// This class is the sanctioned wrapper for the browser timer APIs.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
this._timer = window.setTimeout(() => {
|
||||
this._timer = null;
|
||||
func();
|
||||
@@ -28,6 +30,8 @@ export class Timer {
|
||||
|
||||
public startRepeated(seconds: number, func: () => void): void {
|
||||
this.stop();
|
||||
// This class is the sanctioned wrapper for the browser timer APIs.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
this._timer = window.setInterval(() => {
|
||||
func();
|
||||
}, seconds * 1000);
|
||||
|
||||
@@ -76,7 +76,7 @@ function strip(schema: z.ZodType, cache: Map<z.ZodType, z.ZodType>): z.ZodType {
|
||||
let result: z.ZodType;
|
||||
|
||||
if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault) {
|
||||
// Unwrap the default — don't cache the wrapper itself.
|
||||
// Unwrap the default -- don't cache the wrapper itself.
|
||||
result = strip(toClassic(schema.unwrap()), cache);
|
||||
} else if (schema instanceof z.ZodObject) {
|
||||
const newShape: Record<string, z.core.$ZodType> = {};
|
||||
|
||||
@@ -6,12 +6,12 @@ import { View } from './view';
|
||||
// the target-ID namespace. Must not collide with real camera IDs or media IDs.
|
||||
export const IMAGE_VIEW_TARGET_ID_SENTINEL = '__IMAGE_VIEW__';
|
||||
|
||||
// Returns a universal target identifier for the current view — the single key
|
||||
// Returns a universal target identifier for the current view -- the single key
|
||||
// used by PTZ/zoom state and media retry epochs to identify "what is currently
|
||||
// being displayed." For live, this is the *base* camera ID (substream is an
|
||||
// implementation detail of how to play camera X, not a separate logical
|
||||
// identity — see `getStreamCameraID` for the substream-aware variant used
|
||||
// only inside the playback chain).
|
||||
// identity -- see `getStreamCameraID` for the substream-aware variant used only
|
||||
// inside the playback chain).
|
||||
export const getViewTargetID = (view: View): string | null => {
|
||||
if (view.isViewerView()) {
|
||||
return view.queryResults?.getSelectedResult()?.getID() ?? null;
|
||||
|
||||
Reference in New Issue
Block a user