Add initial keyboard shortcut support.
This commit is contained in:
@@ -1,285 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
ActionType,
|
||||
FrigateCardCustomAction,
|
||||
} from '../config/types.js';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
frigateCardHandleAction,
|
||||
frigateCardHandleActionConfig,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action.js';
|
||||
import { getStreamCameraID } from '../utils/substream.js';
|
||||
import { generateViewContextForZoomChange } from '../components-lib/zoom/zoom-view-context.js';
|
||||
import { CardActionsManagerAPI } from './types.js';
|
||||
|
||||
const interactionSchema = z.object({
|
||||
action: z.enum(['tap', 'double_tap', 'hold', 'start_tap', 'end_tap']),
|
||||
});
|
||||
export type Interaction = z.infer<typeof interactionSchema>;
|
||||
|
||||
const interactionEventSchema = z.object({
|
||||
detail: interactionSchema,
|
||||
});
|
||||
|
||||
export class ActionsManager {
|
||||
protected _api: CardActionsManagerAPI;
|
||||
|
||||
constructor(api: CardActionsManagerAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge card-wide and view-specific actions.
|
||||
* @returns A combined set of action.
|
||||
*/
|
||||
public getMergedActions(): ActionsConfig {
|
||||
const view = this._api.getViewManager().getView();
|
||||
if (this._api.getMessageManager().hasMessage()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
let specificActions: Actions | undefined = undefined;
|
||||
if (view?.is('live')) {
|
||||
specificActions = config?.live.actions;
|
||||
} else if (view?.isGalleryView()) {
|
||||
specificActions = config?.media_gallery?.actions;
|
||||
} else if (view?.isViewerView()) {
|
||||
specificActions = config?.media_viewer.actions;
|
||||
} else if (view?.is('image')) {
|
||||
specificActions = config?.image?.actions;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
return { ...config?.view.actions, ...specificActions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an human interaction called on an element (e.g. 'tap').
|
||||
*/
|
||||
public handleInteractionEvent = (ev: Event): void => {
|
||||
const result = interactionEventSchema.safeParse(ev);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
const interaction = result.data.detail.action;
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
const config = this.getMergedActions();
|
||||
const actionConfig = getActionConfigGivenAction(interaction, config);
|
||||
if (
|
||||
hass &&
|
||||
config &&
|
||||
interaction &&
|
||||
// Don't call frigateCardHandleActionConfig() unless there is explicitly an
|
||||
// action defined (as it uses a default that is unhelpful for views that
|
||||
// have default tap/click actions).
|
||||
actionConfig
|
||||
) {
|
||||
frigateCardHandleActionConfig(
|
||||
this._api.getCardElementManager().getElement(),
|
||||
hass,
|
||||
config,
|
||||
interaction,
|
||||
actionConfig,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
public handleActionEvent = (ev: Event): void => {
|
||||
if (!('detail' in ev)) {
|
||||
// The event may not be a CustomEvent object, see:
|
||||
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
|
||||
return;
|
||||
}
|
||||
|
||||
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
|
||||
if (frigateCardAction) {
|
||||
this.executeFrigateAction(frigateCardAction);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Small convenience method to call frigateCardHandleAction without the caller
|
||||
* needing hass or the element.
|
||||
*/
|
||||
public executeActions(actions: ActionType | ActionType[]): void {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
if (!hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
frigateCardHandleAction(
|
||||
this._api.getCardElementManager().getElement(),
|
||||
hass,
|
||||
{},
|
||||
actions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a card action.
|
||||
* @param frigateCardAction
|
||||
* @returns `true` if an action is executed.
|
||||
*/
|
||||
public async executeFrigateAction(
|
||||
frigateCardAction: FrigateCardCustomAction,
|
||||
): Promise<void> {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
const mediaLoadedInfoManager = this._api.getMediaLoadedInfoManager();
|
||||
|
||||
if (
|
||||
// Command not intended for this card (e.g. query string command).
|
||||
frigateCardAction.card_id &&
|
||||
config?.card_id !== frigateCardAction.card_id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: This function needs to process (view-related) commands even when
|
||||
// _view has not yet been initialized (since it may be used to set a view
|
||||
// via the querystring).
|
||||
const view = this._api.getViewManager().getView();
|
||||
|
||||
const action = frigateCardAction.frigate_card_action;
|
||||
|
||||
switch (action) {
|
||||
case 'default':
|
||||
this._api.getViewManager().setViewDefault();
|
||||
break;
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
case 'image':
|
||||
case 'live':
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
this._api.getViewManager().setViewByParameters({
|
||||
viewName: action,
|
||||
cameraID: view?.camera,
|
||||
});
|
||||
break;
|
||||
case 'download':
|
||||
await this._api.getDownloadManager().downloadViewerMedia();
|
||||
break;
|
||||
case 'camera_ui':
|
||||
this._api.getCameraURLManager().openURL();
|
||||
break;
|
||||
case 'expand':
|
||||
this._api.getExpandManager().toggleExpanded();
|
||||
break;
|
||||
case 'fullscreen':
|
||||
this._api.getFullscreenManager().toggleFullscreen();
|
||||
break;
|
||||
case 'menu_toggle':
|
||||
// This is a rare code path: this would only be used if someone has a
|
||||
// menu toggle action configured outside of the menu itself (e.g.
|
||||
// picture elements).
|
||||
this._api.getCardElementManager().toggleMenu();
|
||||
break;
|
||||
case 'camera_select':
|
||||
const selectCameraID =
|
||||
frigateCardAction.camera ??
|
||||
(frigateCardAction.triggered
|
||||
? this._api.getTriggersManager().getMostRecentlyTriggeredCameraID()
|
||||
: null);
|
||||
if (selectCameraID && view) {
|
||||
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
|
||||
const targetViewName =
|
||||
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
|
||||
this._api.getViewManager().setViewByParameters({
|
||||
viewName: targetViewName,
|
||||
cameraID: selectCameraID,
|
||||
failSafe: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'live_substream_select': {
|
||||
this._api.getViewManager().setViewWithSubstream(frigateCardAction.camera);
|
||||
break;
|
||||
}
|
||||
case 'live_substream_off': {
|
||||
this._api.getViewManager().setViewWithoutSubstream();
|
||||
break;
|
||||
}
|
||||
case 'live_substream_on': {
|
||||
this._api.getViewManager().setViewWithSubstream();
|
||||
break;
|
||||
}
|
||||
case 'media_player':
|
||||
const mediaPlayer = frigateCardAction.media_player;
|
||||
const mediaPlayerController = this._api.getMediaPlayerManager();
|
||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||
|
||||
if (frigateCardAction.media_player_action === 'stop') {
|
||||
await mediaPlayerController.stop(mediaPlayer);
|
||||
} else if (view?.is('live')) {
|
||||
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
||||
} else if (view?.isViewerView() && media) {
|
||||
await mediaPlayerController.playMedia(mediaPlayer, media);
|
||||
}
|
||||
break;
|
||||
case 'diagnostics':
|
||||
this._api.getViewManager().setViewByParameters({ viewName: 'diagnostics' });
|
||||
break;
|
||||
case 'microphone_mute':
|
||||
this._api.getMicrophoneManager().mute();
|
||||
break;
|
||||
case 'microphone_unmute':
|
||||
await this._api.getMicrophoneManager().unmute();
|
||||
break;
|
||||
case 'mute':
|
||||
await mediaLoadedInfoManager.get()?.player?.mute();
|
||||
break;
|
||||
case 'unmute':
|
||||
await mediaLoadedInfoManager.get()?.player?.unmute();
|
||||
break;
|
||||
case 'play':
|
||||
await mediaLoadedInfoManager.get()?.player?.play();
|
||||
break;
|
||||
case 'pause':
|
||||
await mediaLoadedInfoManager.get()?.player?.pause();
|
||||
break;
|
||||
case 'screenshot':
|
||||
await this._api.getDownloadManager().downloadScreenshot();
|
||||
break;
|
||||
case 'display_mode_select':
|
||||
this._api
|
||||
.getViewManager()
|
||||
.setViewWithNewDisplayMode(frigateCardAction.display_mode);
|
||||
break;
|
||||
case 'ptz':
|
||||
const cameraID = this._api.getViewManager().getView()?.camera;
|
||||
if (cameraID) {
|
||||
this._api
|
||||
.getCameraManager()
|
||||
.executePTZAction(cameraID, frigateCardAction.ptz_action, {
|
||||
phase: frigateCardAction.ptz_phase,
|
||||
preset: frigateCardAction.ptz_preset,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'show_ptz':
|
||||
this._api.getViewManager().setViewWithMergedContext({
|
||||
live: { ptzVisible: frigateCardAction.show_ptz },
|
||||
});
|
||||
break;
|
||||
case 'change_zoom':
|
||||
this._api.getViewManager().setViewWithMergedContext(
|
||||
generateViewContextForZoomChange(frigateCardAction.target_id, {
|
||||
zoom: {
|
||||
pan: frigateCardAction.pan,
|
||||
zoom: frigateCardAction.zoom,
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { z } from 'zod';
|
||||
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
|
||||
import { getActionConfigGivenAction } from '../../utils/action.js';
|
||||
import { ActionSet } from './actions/set.js';
|
||||
import { CardActionsManagerAPI } from '../types.js';
|
||||
import { ActionExecutionRequest, AuxillaryActionConfig } from './types.js';
|
||||
import { ActionContext } from 'action';
|
||||
|
||||
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||
|
||||
const interactionSchema = z.object({
|
||||
action: z.enum(INTERACTIONS),
|
||||
});
|
||||
export type Interaction = z.infer<typeof interactionSchema>;
|
||||
|
||||
const interactionEventSchema = z.object({
|
||||
detail: interactionSchema,
|
||||
});
|
||||
|
||||
export class ActionsManager {
|
||||
protected _api: CardActionsManagerAPI;
|
||||
protected _actionsInFlight: ActionSet[] = [];
|
||||
protected _actionContext: ActionContext = {};
|
||||
|
||||
constructor(api: CardActionsManagerAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge card-wide and view-specific actions.
|
||||
* @returns A combined set of action.
|
||||
*/
|
||||
public getMergedActions(): ActionsConfig {
|
||||
const view = this._api.getViewManager().getView();
|
||||
if (this._api.getMessageManager().hasMessage()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
let specificActions: Actions | undefined = undefined;
|
||||
if (view?.is('live')) {
|
||||
specificActions = config?.live.actions;
|
||||
} else if (view?.isGalleryView()) {
|
||||
specificActions = config?.media_gallery?.actions;
|
||||
} else if (view?.isViewerView()) {
|
||||
specificActions = config?.media_viewer.actions;
|
||||
} else if (view?.is('image')) {
|
||||
specificActions = config?.image?.actions;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
return { ...config?.view.actions, ...specificActions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an human interaction called on an element (e.g. 'tap').
|
||||
*/
|
||||
public handleInteractionEvent = (ev: Event): void => {
|
||||
const result = interactionEventSchema.safeParse(ev);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
const interaction = result.data.detail.action;
|
||||
const config = this.getMergedActions();
|
||||
const actionConfig = getActionConfigGivenAction(interaction, config);
|
||||
if (
|
||||
config &&
|
||||
interaction &&
|
||||
// Don't call frigateCardHandleActionConfig() unless there is explicitly an
|
||||
// action defined (as it uses a default that is unhelpful for views that
|
||||
// have default tap/click actions).
|
||||
actionConfig
|
||||
) {
|
||||
this.executeActions(actionConfig, config);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is called when an ll-custom event is fired. This is used by
|
||||
* cards to fire custom actions. This card itself should not call this, but
|
||||
* embedded elements may.
|
||||
*/
|
||||
public handleCustomActionEvent = (ev: Event): void => {
|
||||
if (!('detail' in ev)) {
|
||||
// The event may not be a CustomEvent object, see:
|
||||
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
|
||||
return;
|
||||
}
|
||||
this.executeActions(ev.detail as ActionType);
|
||||
};
|
||||
|
||||
/**
|
||||
* This method handles actions requested by components of the Frigate card
|
||||
* itself (e.g. menu, PTZ controller).
|
||||
*/
|
||||
public handleActionExecutionRequestEvent = async (
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
): Promise<void> => {
|
||||
await this.executeActions(ev.detail.action, ev.detail.config);
|
||||
};
|
||||
|
||||
public uninitialize(): void {
|
||||
// If there are any long-running actions, ensure they are stopped.
|
||||
this._actionsInFlight.forEach((actionSet) => actionSet.stop());
|
||||
}
|
||||
|
||||
public async executeActions(
|
||||
action: ActionType | ActionType[],
|
||||
config?: AuxillaryActionConfig,
|
||||
): Promise<void> {
|
||||
const actionSet = new ActionSet(this._actionContext, action, {
|
||||
config: config,
|
||||
cardID: this._api.getConfigManager().getConfig()?.card_id,
|
||||
});
|
||||
|
||||
this._actionsInFlight.push(actionSet);
|
||||
await actionSet.execute(this._api);
|
||||
this._actionsInFlight = this._actionsInFlight.filter((a) => a !== actionSet);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { FrigateCardCustomAction } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { Action, AuxillaryActionConfig } from '../types';
|
||||
|
||||
export class BaseAction<T> implements Action {
|
||||
protected _context: ActionContext;
|
||||
protected _action: T;
|
||||
protected _config?: AuxillaryActionConfig;
|
||||
|
||||
constructor(context: ActionContext, action: T, config?: AuxillaryActionConfig) {
|
||||
this._context = context;
|
||||
this._action = action;
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async execute(_api: CardActionsAPI): Promise<void> {
|
||||
// Pass.
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
// Pass.
|
||||
}
|
||||
}
|
||||
|
||||
export class FrigateCardAction<
|
||||
T extends FrigateCardCustomAction,
|
||||
> extends BaseAction<T> {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CameraSelectActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class CameraSelectAction extends FrigateCardAction<CameraSelectActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const selectCameraID =
|
||||
this._action.camera ??
|
||||
(this._action.triggered
|
||||
? api.getTriggersManager().getMostRecentlyTriggeredCameraID()
|
||||
: null);
|
||||
const view = api.getViewManager().getView();
|
||||
const config = api.getConfigManager().getConfig();
|
||||
|
||||
if (selectCameraID && view) {
|
||||
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
|
||||
const targetViewName =
|
||||
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
|
||||
api.getViewManager().setViewByParameters({
|
||||
viewName: targetViewName,
|
||||
cameraID: selectCameraID,
|
||||
failSafe: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class CameraUIAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getCameraURLManager().openURL();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class DefaultAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { DisplayModeActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class DisplayModeSelectAction extends FrigateCardAction<DisplayModeActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getViewManager().setViewWithNewDisplayMode(this._action.display_mode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class DownloadAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getDownloadManager().downloadViewerMedia();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class ExpandAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getExpandManager().toggleExpanded();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class FullscreenAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getFullscreenManager().toggleFullscreen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ActionConfig, handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { BaseAction } from './base';
|
||||
|
||||
/**
|
||||
* Handles generic HA (non-Frigate) actions (e.g. 'more-info')
|
||||
*/
|
||||
export class GenericAction extends BaseAction<ActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const hass = api.getHASSManager().getHASS();
|
||||
if (hass) {
|
||||
handleActionConfig(
|
||||
api.getCardElementManager().getElement(),
|
||||
hass,
|
||||
this._config ?? {},
|
||||
this._action,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { LogActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class LogAction extends FrigateCardAction<LogActionConfig> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async execute(_api: CardActionsAPI): Promise<void> {
|
||||
console[this._action.level](this._action.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MediaPlayerActionConfig } from '../../../config/types';
|
||||
import { getStreamCameraID } from '../../../utils/substream';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class MediaPlayerAction extends FrigateCardAction<MediaPlayerActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const mediaPlayer = this._action.media_player;
|
||||
const mediaPlayerController = api.getMediaPlayerManager();
|
||||
const view = api.getViewManager().getView();
|
||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||
|
||||
if (this._action.media_player_action === 'stop') {
|
||||
await mediaPlayerController.stop(mediaPlayer);
|
||||
} else if (view?.is('live')) {
|
||||
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
||||
} else if (view?.isViewerView() && media) {
|
||||
await mediaPlayerController.playMedia(mediaPlayer, media);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class MenuToggleAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getCardElementManager().toggleMenu();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class MicrophoneMuteAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getMicrophoneManager().mute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class MicrophoneUnmuteAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getMicrophoneManager().unmute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class MuteAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getMediaLoadedInfoManager().get()?.player?.mute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class PauseAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getMediaLoadedInfoManager().get()?.player?.pause();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class PlayAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getMediaLoadedInfoManager().get()?.player?.play();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { PTZControlsActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class PTZControlsAction extends FrigateCardAction<PTZControlsActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewWithMergedContext({
|
||||
ptzControls: { enabled: this._action.enabled },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import clamp from 'lodash-es/clamp';
|
||||
import {
|
||||
PartialZoomSettings,
|
||||
ZOOM_DEFAULT_PAN_X,
|
||||
ZOOM_DEFAULT_PAN_Y,
|
||||
ZOOM_DEFAULT_SCALE,
|
||||
} from '../../../components-lib/zoom/types';
|
||||
import { generateViewContextForZoom } from '../../../components-lib/zoom/zoom-view-context';
|
||||
import { PTZDigitialActionConfig, ZOOM_MAX, ZOOM_MIN } from '../../../config/types';
|
||||
import { getPTZTarget } from '../../../utils/ptz';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
import { TargetedActionContext } from '../types';
|
||||
import {
|
||||
setInProgressForThisTarget,
|
||||
stopInProgressForThisTarget,
|
||||
} from '../utils/action-state';
|
||||
|
||||
const STEP_DELAY_SECONDS = 0.1;
|
||||
const STEP_ZOOM = 0.1;
|
||||
const STEP_PAN = 5;
|
||||
|
||||
declare module 'action' {
|
||||
interface ActionContext {
|
||||
ptzDigital?: TargetedActionContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class PTZDigitalAction extends FrigateCardAction<PTZDigitialActionConfig> {
|
||||
protected _timer = new Timer();
|
||||
|
||||
protected async _stepChange(api: CardActionsAPI, targetID: string): Promise<void> {
|
||||
api.getViewManager().setViewWithMergedContext(
|
||||
generateViewContextForZoom(targetID, {
|
||||
requested: this._convertActionToZoomSettings(
|
||||
api.getViewManager().getView()?.context?.zoom?.[targetID]?.observed,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
this._timer.stop();
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const view = api.getViewManager().getView();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetID =
|
||||
this._action.target_id ??
|
||||
getPTZTarget(view, { type: 'digital', cameraManager: api.getCameraManager() })
|
||||
?.targetID;
|
||||
if (!targetID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!this._action.absolute || !this._action.ptz_phase) {
|
||||
return await this._stepChange(api, targetID);
|
||||
}
|
||||
|
||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||
if (this._action.ptz_phase === 'start') {
|
||||
stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||
setInProgressForThisTarget(targetID, this._context, 'ptzDigital', this);
|
||||
|
||||
await this._stepChange(api, targetID);
|
||||
this._timer.startRepeated(STEP_DELAY_SECONDS, () =>
|
||||
this._stepChange(api, targetID),
|
||||
);
|
||||
} else if (this._action.ptz_phase === 'stop') {
|
||||
stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||
delete this._context.ptzDigital?.[targetID];
|
||||
}
|
||||
}
|
||||
|
||||
protected _convertActionToZoomSettings(
|
||||
base?: PartialZoomSettings,
|
||||
): PartialZoomSettings {
|
||||
if (!this._action.absolute && !this._action.ptz_action) {
|
||||
// If neither an absolute position nor an action are specified, the request
|
||||
// is assumed to be to return to default.
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this._action.absolute) {
|
||||
return this._action.absolute;
|
||||
}
|
||||
|
||||
const zoom = base?.zoom ?? ZOOM_DEFAULT_SCALE;
|
||||
const pan = {
|
||||
x: base?.pan?.x ?? ZOOM_DEFAULT_PAN_X,
|
||||
y: base?.pan?.y ?? ZOOM_DEFAULT_PAN_Y,
|
||||
};
|
||||
|
||||
const zoomDelta =
|
||||
this._action.ptz_action === 'zoom_in'
|
||||
? STEP_ZOOM
|
||||
: this._action.ptz_action === 'zoom_out'
|
||||
? -STEP_ZOOM
|
||||
: 0;
|
||||
const xDelta =
|
||||
this._action.ptz_action === 'left'
|
||||
? -STEP_PAN
|
||||
: this._action.ptz_action === 'right'
|
||||
? STEP_PAN
|
||||
: 0;
|
||||
const yDelta =
|
||||
this._action.ptz_action === 'up'
|
||||
? -STEP_PAN
|
||||
: this._action.ptz_action === 'down'
|
||||
? STEP_PAN
|
||||
: 0;
|
||||
|
||||
return {
|
||||
zoom: clamp(zoom + zoomDelta, ZOOM_MIN, ZOOM_MAX),
|
||||
pan: {
|
||||
x: clamp(pan.x + xDelta, 0, 100),
|
||||
y: clamp(pan.y + yDelta, 0, 100),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { PTZMultiActionConfig } from '../../../config/types';
|
||||
import { createPTZAction, createPTZDigitalAction } from '../../../utils/action';
|
||||
import { PTZType, getPTZTarget, hasCameraTruePTZ } from '../../../utils/ptz';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
import { PTZAction } from './ptz';
|
||||
import { PTZDigitalAction } from './ptz-digital';
|
||||
|
||||
export class PTZMultiAction extends FrigateCardAction<PTZMultiActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const view = api.getViewManager().getView();
|
||||
let targetID: string | null = null;
|
||||
let type: PTZType | null = null;
|
||||
|
||||
if (this._action.target_id) {
|
||||
targetID = this._action.target_id;
|
||||
type = hasCameraTruePTZ(api.getCameraManager(), targetID) ? 'ptz' : 'digital';
|
||||
} else if (view) {
|
||||
const multiTarget = getPTZTarget(view, { cameraManager: api.getCameraManager() });
|
||||
targetID = multiTarget?.targetID ?? null;
|
||||
type = multiTarget?.type ?? null;
|
||||
}
|
||||
|
||||
if (!targetID || type === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
(type === 'ptz'
|
||||
? this._toPTZAction(targetID)
|
||||
: this._toPTZDigitalAction(targetID)
|
||||
).execute(api);
|
||||
}
|
||||
|
||||
protected _toPTZAction(targetID: string): PTZAction {
|
||||
return new PTZAction(
|
||||
this._context,
|
||||
createPTZAction({
|
||||
cardID: this._action.card_id,
|
||||
cameraID: targetID,
|
||||
ptzAction: this._action.ptz_action,
|
||||
ptzPhase: this._action.ptz_phase,
|
||||
ptzPreset: this._action.ptz_preset,
|
||||
}),
|
||||
this._config,
|
||||
);
|
||||
}
|
||||
|
||||
protected _toPTZDigitalAction(targetID: string): PTZDigitalAction {
|
||||
return new PTZDigitalAction(
|
||||
this._context,
|
||||
createPTZDigitalAction({
|
||||
cardID: this._action.card_id,
|
||||
ptzPhase: this._action.ptz_phase,
|
||||
ptzAction: this._action.ptz_action,
|
||||
targetID: targetID,
|
||||
}),
|
||||
this._config,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { PTZActionConfig } from '../../../config/types';
|
||||
import { getPTZTarget, ptzActionToCapabilityKey } from '../../../utils/ptz';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import {
|
||||
setInProgressForThisTarget,
|
||||
stopInProgressForThisTarget,
|
||||
} from '../utils/action-state';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
interface PTZContext {
|
||||
[cameraID: string]: {
|
||||
inProgressAction?: PTZAction;
|
||||
};
|
||||
}
|
||||
|
||||
declare module 'action' {
|
||||
interface ActionContext {
|
||||
ptz?: PTZContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class PTZAction extends FrigateCardAction<PTZActionConfig> {
|
||||
protected _timer = new Timer();
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
this._timer.stop();
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
const view = api.getViewManager().getView();
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ptzCameraID =
|
||||
this._action.camera ??
|
||||
getPTZTarget(view, { type: 'ptz', cameraManager: api.getCameraManager() })
|
||||
?.targetID ??
|
||||
null;
|
||||
const ptzCapabilities = ptzCameraID
|
||||
? api
|
||||
.getCameraManager()
|
||||
.getCameraCapabilities(ptzCameraID)
|
||||
?.getPTZCapabilities()
|
||||
: null;
|
||||
const ptzConfiguration = ptzCameraID
|
||||
? api.getCameraManager().getStore().getCameraConfig(ptzCameraID)?.ptz
|
||||
: null;
|
||||
if (!ptzCameraID || !ptzCapabilities || !ptzConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._action.ptz_action) {
|
||||
if (ptzCapabilities.presets && ptzCapabilities.presets.length >= 1) {
|
||||
await api.getCameraManager().executePTZAction(ptzCameraID, 'preset', {
|
||||
phase: this._action.ptz_phase,
|
||||
preset: ptzCapabilities.presets[0],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const capabilityKey = ptzActionToCapabilityKey(this._action.ptz_action);
|
||||
if (
|
||||
(capabilityKey &&
|
||||
ptzCapabilities[capabilityKey]?.includes(
|
||||
this._action.ptz_phase ? 'continuous' : 'relative',
|
||||
)) ||
|
||||
this._action.ptz_action === 'preset'
|
||||
) {
|
||||
// Scenario: Camera natively supports requested move type.
|
||||
return await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
phase: this._action.ptz_phase,
|
||||
preset: this._action.ptz_preset,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._action.ptz_phase === 'start') {
|
||||
// Scenario: Asked to start a continuous move, camera only supports relative moves natively.
|
||||
stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||
setInProgressForThisTarget(ptzCameraID, this._context, 'ptz', this);
|
||||
|
||||
const singleStep = async (): Promise<void> => {
|
||||
this._action.ptz_action &&
|
||||
(await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
}));
|
||||
// Only start the timer for the next step after this step returns.
|
||||
this._timer.start(ptzConfiguration.r2c_delay_between_calls_seconds, singleStep);
|
||||
};
|
||||
|
||||
await singleStep();
|
||||
} else if (this._action.ptz_phase === 'stop') {
|
||||
// Scenario: Asked to stop continuous move, camera only supports relative moves natively.
|
||||
stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||
} else {
|
||||
// Relative move (but camera only supports continuous).
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
phase: 'start',
|
||||
});
|
||||
|
||||
this._timer.start(ptzConfiguration.c2r_delay_between_calls_seconds, async () => {
|
||||
this._action.ptz_action &&
|
||||
(await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
phase: 'stop',
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class ScreenshotAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getDownloadManager().downloadScreenshot();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ActionContext } from 'action';
|
||||
import { ActionType } from '../../../config/types';
|
||||
import { arrayify } from '../../../utils/basic';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { ActionFactory } from '../factory';
|
||||
import { Action, AuxillaryActionConfig } from '../types';
|
||||
|
||||
export class ActionSet implements Action {
|
||||
protected _context: ActionContext;
|
||||
protected _actions: Action[] = [];
|
||||
protected _factory = new ActionFactory();
|
||||
protected _stopped = false;
|
||||
|
||||
constructor(
|
||||
context: ActionContext,
|
||||
actions: ActionType | ActionType[],
|
||||
options?: {
|
||||
config?: AuxillaryActionConfig;
|
||||
cardID?: string;
|
||||
},
|
||||
) {
|
||||
this._context = context;
|
||||
for (const actionObj of arrayify(actions)) {
|
||||
const action = this._factory.createAction(context, actionObj, options);
|
||||
if (action) {
|
||||
this._actions.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
for (const action of this._actions) {
|
||||
if (this._stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
await action.execute(api);
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
this._stopped = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { SleepActionConfig } from '../../../config/types';
|
||||
import { sleep } from '../../../utils/basic';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { timeDeltaToSeconds } from '../utils/time-delta';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class SleepAction extends FrigateCardAction<SleepActionConfig> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async execute(_api: CardActionsAPI): Promise<void> {
|
||||
await sleep(timeDeltaToSeconds(this._action.duration));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class SubstreamOffAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewWithoutSubstream();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class SubstreamOnAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewWithSubstream();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SubstreamSelectActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class SubstreamSelectAction extends FrigateCardAction<SubstreamSelectActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewWithSubstream(this._action.camera);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { GeneralActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class UnmuteAction extends FrigateCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await api.getMediaLoadedInfoManager().get()?.player?.unmute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ViewActionConfig } from '../../../config/types';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { FrigateCardAction } from './base';
|
||||
|
||||
export class ViewAction extends FrigateCardAction<ViewActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
api.getViewManager().setViewByParameters({
|
||||
viewName: this._action.frigate_card_action,
|
||||
|
||||
// Note: This function needs to process (view-related) commands even when
|
||||
// _view has not yet been initialized (since it may be used to set a view
|
||||
// via the querystring).
|
||||
cameraID: api.getViewManager().getView()?.camera,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { ActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||
import { ActionContext } from 'action';
|
||||
import { ActionType } from '../../config/types';
|
||||
import { convertActionToCardCustomAction } from '../../utils/action';
|
||||
import { CameraSelectAction } from './actions/camera-select';
|
||||
import { CameraUIAction } from './actions/camera-ui';
|
||||
import { DefaultAction } from './actions/default';
|
||||
import { DisplayModeSelectAction } from './actions/display-mode-select';
|
||||
import { DownloadAction } from './actions/download';
|
||||
import { ExpandAction } from './actions/expand';
|
||||
import { FullscreenAction } from './actions/fullscreen';
|
||||
import { GenericAction } from './actions/generic';
|
||||
import { LogAction } from './actions/log';
|
||||
import { MediaPlayerAction } from './actions/media-player';
|
||||
import { MenuToggleAction } from './actions/menu-toggle';
|
||||
import { MicrophoneMuteAction } from './actions/microphone-mute';
|
||||
import { MicrophoneUnmuteAction } from './actions/microphone-unmute';
|
||||
import { MuteAction } from './actions/mute';
|
||||
import { PauseAction } from './actions/pause';
|
||||
import { PlayAction } from './actions/play';
|
||||
import { PTZAction } from './actions/ptz';
|
||||
import { PTZDigitalAction } from './actions/ptz-digital';
|
||||
import { PTZMultiAction } from './actions/ptz-multi';
|
||||
import { ScreenshotAction } from './actions/screenshot';
|
||||
import { PTZControlsAction } from './actions/ptz-controls';
|
||||
import { SleepAction } from './actions/sleep';
|
||||
import { SubstreamOffAction } from './actions/substream-off';
|
||||
import { SubstreamOnAction } from './actions/substream-on';
|
||||
import { SubstreamSelectAction } from './actions/substream-select';
|
||||
import { UnmuteAction } from './actions/unmute';
|
||||
import { ViewAction } from './actions/view';
|
||||
import { Action, AuxillaryActionConfig } from './types';
|
||||
|
||||
export class ActionFactory {
|
||||
public createAction(
|
||||
context: ActionContext,
|
||||
action: ActionType,
|
||||
options?: {
|
||||
config?: AuxillaryActionConfig;
|
||||
cardID?: string;
|
||||
},
|
||||
): Action | null {
|
||||
const frigateCardAction = convertActionToCardCustomAction(action);
|
||||
if (action.action !== 'fire-dom-event' || !frigateCardAction) {
|
||||
// * There is a slight typing (but not functional) difference between
|
||||
// ActionType in this card and ActionConfig in `custom-card-helpers`. See
|
||||
// `ExtendedConfirmationRestrictionConfig` in `types.ts` for the source and
|
||||
// reason behind this difference.
|
||||
return new GenericAction(context, action as ActionConfig, options?.config);
|
||||
}
|
||||
|
||||
if (
|
||||
// Command not intended for this card (e.g. query string command).
|
||||
frigateCardAction.card_id &&
|
||||
frigateCardAction.card_id !== options?.cardID
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (frigateCardAction.frigate_card_action) {
|
||||
case 'default':
|
||||
return new DefaultAction(context, frigateCardAction, options?.config);
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
case 'image':
|
||||
case 'live':
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
case 'diagnostics':
|
||||
return new ViewAction(context, frigateCardAction, options?.config);
|
||||
case 'sleep':
|
||||
return new SleepAction(context, frigateCardAction, options?.config);
|
||||
case 'download':
|
||||
return new DownloadAction(context, frigateCardAction, options?.config);
|
||||
case 'camera_ui':
|
||||
return new CameraUIAction(context, frigateCardAction, options?.config);
|
||||
case 'expand':
|
||||
return new ExpandAction(context, frigateCardAction, options?.config);
|
||||
case 'fullscreen':
|
||||
return new FullscreenAction(context, frigateCardAction, options?.config);
|
||||
case 'menu_toggle':
|
||||
// This is a rare code path: this would only be used if someone has a
|
||||
// menu toggle action configured outside of the menu itself.
|
||||
return new MenuToggleAction(context, frigateCardAction, options?.config);
|
||||
case 'camera_select':
|
||||
return new CameraSelectAction(context, frigateCardAction, options?.config);
|
||||
case 'live_substream_select':
|
||||
return new SubstreamSelectAction(context, frigateCardAction, options?.config);
|
||||
case 'live_substream_off':
|
||||
return new SubstreamOffAction(context, frigateCardAction, options?.config);
|
||||
case 'live_substream_on':
|
||||
return new SubstreamOnAction(context, frigateCardAction, options?.config);
|
||||
case 'media_player':
|
||||
return new MediaPlayerAction(context, frigateCardAction, options?.config);
|
||||
case 'microphone_mute':
|
||||
return new MicrophoneMuteAction(context, frigateCardAction, options?.config);
|
||||
case 'microphone_unmute':
|
||||
return new MicrophoneUnmuteAction(context, frigateCardAction, options?.config);
|
||||
case 'mute':
|
||||
return new MuteAction(context, frigateCardAction, options?.config);
|
||||
case 'unmute':
|
||||
return new UnmuteAction(context, frigateCardAction, options?.config);
|
||||
case 'play':
|
||||
return new PlayAction(context, frigateCardAction, options?.config);
|
||||
case 'pause':
|
||||
return new PauseAction(context, frigateCardAction, options?.config);
|
||||
case 'screenshot':
|
||||
return new ScreenshotAction(context, frigateCardAction, options?.config);
|
||||
case 'display_mode_select':
|
||||
return new DisplayModeSelectAction(context, frigateCardAction, options?.config);
|
||||
case 'ptz':
|
||||
return new PTZAction(context, frigateCardAction, options?.config);
|
||||
case 'ptz_digital':
|
||||
return new PTZDigitalAction(context, frigateCardAction, options?.config);
|
||||
case 'ptz_multi':
|
||||
return new PTZMultiAction(context, frigateCardAction, options?.config);
|
||||
case 'ptz_controls':
|
||||
return new PTZControlsAction(context, frigateCardAction, options?.config);
|
||||
case 'log':
|
||||
return new LogAction(context, frigateCardAction, options?.config);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
console.warn(
|
||||
`Frigate card received unknown card action: ${frigateCardAction['frigate_card_action']}`,
|
||||
);
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ActionType } from "../../config/types";
|
||||
import { CardActionsAPI } from "../types";
|
||||
|
||||
export interface AuxillaryActionConfig {
|
||||
camera_image?: string;
|
||||
entity?: string;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
execute(api: CardActionsAPI): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ActionExecutionRequest {
|
||||
action: ActionType[] | ActionType;
|
||||
config?: AuxillaryActionConfig;
|
||||
}
|
||||
|
||||
export interface TargetedActionContext {
|
||||
[targetID: string]: {
|
||||
inProgressAction?: Action;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import merge from 'lodash-es/merge';
|
||||
import { Action, TargetedActionContext } from '../types';
|
||||
import { ActionContext } from 'action';
|
||||
|
||||
export const stopInProgressForThisTarget = (
|
||||
targetID: string,
|
||||
context?: TargetedActionContext,
|
||||
): void => {
|
||||
context?.[targetID]?.inProgressAction?.stop();
|
||||
};
|
||||
|
||||
export const setInProgressForThisTarget = (
|
||||
targetID: string,
|
||||
context: ActionContext,
|
||||
contextKey: keyof ActionContext,
|
||||
action: Action,
|
||||
) => {
|
||||
merge(context, {
|
||||
[contextKey]: {
|
||||
[targetID]: {
|
||||
inProgressAction: action,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { dispatchFrigateCardEvent } from '../../../utils/basic';
|
||||
import { ActionExecutionRequest } from '../types';
|
||||
|
||||
export const dispatchActionExecutionRequest = (
|
||||
element: HTMLElement,
|
||||
request: ActionExecutionRequest,
|
||||
) => {
|
||||
dispatchFrigateCardEvent(element, 'action:execution-request', request);
|
||||
};
|
||||
|
||||
export interface ActionExecutionRequestEventTarget extends EventTarget {
|
||||
addEventListener(
|
||||
event: 'frigate-card:action:execution-request',
|
||||
listener: (
|
||||
this: ActionExecutionRequestEventTarget,
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
addEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
removeEventListener(
|
||||
event: 'frigate-card:action:execution-request',
|
||||
listener: (
|
||||
this: ActionExecutionRequestEventTarget,
|
||||
ev: CustomEvent<ActionExecutionRequest>,
|
||||
) => void,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
removeEventListener(
|
||||
type: string,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TimeDelta } from '../../../config/types';
|
||||
|
||||
export const timeDeltaToSeconds = (timeDelta: TimeDelta): number => {
|
||||
return (
|
||||
(timeDelta.h ?? 0) * 3600 +
|
||||
(timeDelta.m ?? 0) * 60 +
|
||||
(timeDelta.s ?? 0) +
|
||||
(timeDelta.ms ?? 0) / 1000
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Automation, AutomationActions, Automations } from '../config/types.js';
|
||||
import { Automation, AutomationActions } from '../config/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { frigateCardHandleAction } from '../utils/action.js';
|
||||
import { CardAutomationsAPI } from './types.js';
|
||||
import { CardAutomationsAPI, TaggedAutomations } from './types.js';
|
||||
|
||||
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
||||
|
||||
export class AutomationsManager {
|
||||
protected _api: CardAutomationsAPI;
|
||||
|
||||
protected _automations: Automations;
|
||||
protected _automations: TaggedAutomations = [];
|
||||
protected _priorEvaluations: Map<Automation, boolean> = new Map();
|
||||
|
||||
// A counter to avoid infinite loops, increases every time actions are run,
|
||||
@@ -19,10 +18,12 @@ export class AutomationsManager {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public setAutomationsFromConfig() {
|
||||
this._automations = this._api
|
||||
.getConfigManager()
|
||||
.getNonOverriddenConfig()?.automations;
|
||||
public deleteAutomations(tag?: unknown) {
|
||||
this._automations = this._automations.filter((automation) => automation.tag !== tag);
|
||||
}
|
||||
|
||||
public addAutomations(automations: TaggedAutomations): void {
|
||||
this._automations.push(...automations);
|
||||
}
|
||||
|
||||
public execute(): void {
|
||||
@@ -34,8 +35,8 @@ export class AutomationsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionsToRun: AutomationActions[] = [];
|
||||
for (const automation of this._automations ?? []) {
|
||||
const actionsToRun: AutomationActions = [];
|
||||
for (const automation of this._automations) {
|
||||
const shouldExecute = this._api
|
||||
.getConditionsManager()
|
||||
.evaluateConditions(automation.conditions);
|
||||
@@ -43,27 +44,27 @@ export class AutomationsManager {
|
||||
const priorEvaluation = this._priorEvaluations.get(automation);
|
||||
this._priorEvaluations.set(automation, shouldExecute);
|
||||
if (shouldExecute !== priorEvaluation && actions) {
|
||||
actionsToRun.push(actions);
|
||||
actionsToRun.push(...actions);
|
||||
}
|
||||
}
|
||||
|
||||
++this._nestedAutomationExecutions;
|
||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||
type: 'error',
|
||||
message: localize('error.too_many_automations'),
|
||||
});
|
||||
if (!actionsToRun.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionsToRun.forEach((actions) => {
|
||||
frigateCardHandleAction(
|
||||
this._api.getCardElementManager().getElement(),
|
||||
hass,
|
||||
{},
|
||||
actions,
|
||||
);
|
||||
});
|
||||
--this._nestedAutomationExecutions;
|
||||
const runActions = async (actions: AutomationActions): Promise<void> => {
|
||||
++this._nestedAutomationExecutions;
|
||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||
type: 'error',
|
||||
message: localize('error.too_many_automations'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this._api.getActionsManager().executeActions(actions);
|
||||
--this._nestedAutomationExecutions;
|
||||
};
|
||||
runActions(actionsToRun);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import { setOrRemoveAttribute } from '../utils/basic';
|
||||
import { isCardInPanel } from '../utils/ha';
|
||||
import { InitializationAspect } from './initialization-manager';
|
||||
import { CardElementAPI } from './types';
|
||||
import { ActionExecutionRequestEventTarget } from './actions/utils/execution-request';
|
||||
|
||||
export type ScrollCallback = () => void;
|
||||
export type MenuToggleCallback = () => void;
|
||||
|
||||
export type CardHTMLElement = LitElement & ReactiveControllerHost & ActionEventTarget;
|
||||
export type CardHTMLElement = LitElement & ReactiveControllerHost & ActionEventTarget & ActionExecutionRequestEventTarget;
|
||||
|
||||
export class CardElementManager {
|
||||
protected _api: CardElementAPI;
|
||||
@@ -62,19 +63,21 @@ export class CardElementManager {
|
||||
this._api.getExpandManager().initialize();
|
||||
this._api.getMediaLoadedInfoManager().initialize();
|
||||
this._api.getMicrophoneManager().initialize();
|
||||
this._api.getKeyboardStateManager().initialize();
|
||||
|
||||
// Whether or not the card is in panel mode on the dashboard.
|
||||
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
||||
setOrRemoveAttribute(this._element, true, 'tabindex', '0');
|
||||
|
||||
this._api.getFullscreenManager().connect();
|
||||
|
||||
|
||||
this._element.addEventListener(
|
||||
'mousemove',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
);
|
||||
this._element.addEventListener(
|
||||
'll-custom',
|
||||
this._api.getActionsManager().handleActionEvent,
|
||||
this._api.getActionsManager().handleCustomActionEvent,
|
||||
);
|
||||
this._element.addEventListener(
|
||||
'action',
|
||||
@@ -84,6 +87,10 @@ export class CardElementManager {
|
||||
'action',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
);
|
||||
this._element.addEventListener(
|
||||
'frigate-card:action:execution-request',
|
||||
this._api.getActionsManager().handleActionExecutionRequestEvent,
|
||||
);
|
||||
|
||||
// Listen for HA `navigate` actions.
|
||||
// See: https://github.com/home-assistant/frontend/blob/273992c8e9c3062c6e49481b6d7d688a07067232/src/common/navigate.ts#L43
|
||||
@@ -106,23 +113,25 @@ export class CardElementManager {
|
||||
|
||||
public elementDisconnected(): void {
|
||||
setOrRemoveAttribute(this._element, false, 'panel');
|
||||
setOrRemoveAttribute(this._element, false, 'tabindex');
|
||||
|
||||
// When the dashboard 'tab' is changed, the media is effectively unloaded.
|
||||
this._api.getMediaLoadedInfoManager().clear();
|
||||
this._api.getFullscreenManager().disconnect();
|
||||
this._api.getKeyboardStateManager().uninitialize();
|
||||
this._api.getActionsManager().uninitialize();
|
||||
|
||||
// Uninitialize cameras to cause them to reinitialize on
|
||||
// reconnection, to ensure the state subscription/unsubscription works
|
||||
// correctly for triggers.
|
||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS),
|
||||
|
||||
this._element.removeEventListener(
|
||||
'mousemove',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
);
|
||||
this._element.removeEventListener(
|
||||
'mousemove',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
);
|
||||
this._element.removeEventListener(
|
||||
'll-custom',
|
||||
this._api.getActionsManager().handleActionEvent,
|
||||
this._api.getActionsManager().handleCustomActionEvent,
|
||||
);
|
||||
this._element.removeEventListener(
|
||||
'action',
|
||||
@@ -132,6 +141,10 @@ export class CardElementManager {
|
||||
'action',
|
||||
this._api.getInteractionManager().reportInteraction,
|
||||
);
|
||||
this._element.removeEventListener(
|
||||
'frigate-card:action:execution-request',
|
||||
this._api.getActionsManager().handleActionExecutionRequestEvent,
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'location-changed',
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Overrides,
|
||||
} from '../config/types';
|
||||
import { desparsifyArrays } from '../utils/basic';
|
||||
import { CardConditionAPI } from './types';
|
||||
import { CardConditionAPI, KeysState } from './types';
|
||||
|
||||
interface MicrophoneConditionState {
|
||||
connected?: boolean;
|
||||
@@ -35,6 +35,7 @@ interface ConditionState {
|
||||
interaction?: boolean;
|
||||
microphone?: MicrophoneConditionState;
|
||||
user?: CurrentUser;
|
||||
keys?: KeysState;
|
||||
}
|
||||
|
||||
export class ConditionsEvaluateRequestEvent extends Event {
|
||||
@@ -190,7 +191,9 @@ export class ConditionsManager {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
const conditions: FrigateCardCondition[] = [];
|
||||
config?.overrides?.forEach((override) => conditions.push(...override.conditions));
|
||||
config?.automations?.forEach((automation) => conditions.push(...automation.conditions));
|
||||
config?.automations?.forEach((automation) =>
|
||||
conditions.push(...automation.conditions),
|
||||
);
|
||||
|
||||
// Element conditions can be arbitrarily nested underneath conditionals and
|
||||
// custom elements that this card may not known. Here we recursively parse
|
||||
@@ -323,6 +326,20 @@ export class ConditionsManager {
|
||||
(conditionObj.muted === undefined ||
|
||||
state.microphone?.muted === conditionObj.muted)
|
||||
);
|
||||
case 'key':
|
||||
return (
|
||||
!!state.keys &&
|
||||
conditionObj.key in state.keys &&
|
||||
(conditionObj.state ?? 'down') === state.keys[conditionObj.key].state &&
|
||||
(conditionObj.ctrl === undefined ||
|
||||
conditionObj.ctrl === !!state.keys[conditionObj.key].ctrl) &&
|
||||
(conditionObj.alt === undefined ||
|
||||
conditionObj.alt === !!state.keys[conditionObj.key].alt) &&
|
||||
(conditionObj.meta === undefined ||
|
||||
conditionObj.meta === !!state.keys[conditionObj.key].meta) &&
|
||||
(conditionObj.shift === undefined ||
|
||||
conditionObj.shift === !!state.keys[conditionObj.key].shift)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-9
@@ -1,17 +1,19 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isConfigUpgradeable } from '../config/management';
|
||||
import { isConfigUpgradeable } from '../../config/management.js';
|
||||
import {
|
||||
CardWideConfig,
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigSchema,
|
||||
RawFrigateCardConfig,
|
||||
} from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { setProfiles } from '../config/profiles';
|
||||
import { getParseErrorPaths } from '../utils/zod.js';
|
||||
import { getOverriddenConfig } from './conditions-manager';
|
||||
import { InitializationAspect } from './initialization-manager';
|
||||
import { CardConfigAPI } from './types';
|
||||
} from '../../config/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import { setProfiles } from '../../config/profiles/index.js';
|
||||
import { getParseErrorPaths } from '../../utils/zod.js';
|
||||
import { getOverriddenConfig } from '../conditions-manager.js';
|
||||
import { InitializationAspect } from '../initialization-manager.js';
|
||||
import { CardConfigAPI } from '../types.js';
|
||||
import { setAutomationsFromConfig } from './load-automations.js';
|
||||
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
|
||||
|
||||
export class ConfigManager {
|
||||
protected _api: CardConfigAPI;
|
||||
@@ -92,9 +94,10 @@ export class ConfigManager {
|
||||
this._api.getMediaLoadedInfoManager().clear();
|
||||
this._api.getViewManager().reset();
|
||||
this._api.getMessageManager().reset();
|
||||
this._api.getAutomationsManager().setAutomationsFromConfig();
|
||||
this._api.getStyleManager().setPerformance();
|
||||
this._api.getCardElementManager().update();
|
||||
setKeyboardShortcutsFromConfig(this._api, this);
|
||||
setAutomationsFromConfig(this._api);
|
||||
|
||||
this.computeOverrideConfig();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { CardConfigLoaderAPI } from '../types';
|
||||
|
||||
export const setAutomationsFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
api.getAutomationsManager().deleteAutomations();
|
||||
api
|
||||
.getAutomationsManager()
|
||||
.addAutomations(api.getConfigManager().getNonOverriddenConfig()?.automations ?? []);
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
KeyboardShortcuts,
|
||||
PTZKeyboardShortcutName,
|
||||
} from '../../config/keyboard-shortcuts';
|
||||
import { PTZAction } from '../../config/ptz';
|
||||
import { CardConfigLoaderAPI, TaggedAutomations } from '../types';
|
||||
import { createPTZMultiAction } from '../../utils/action';
|
||||
|
||||
export const setKeyboardShortcutsFromConfig = (
|
||||
api: CardConfigLoaderAPI,
|
||||
tag: unknown,
|
||||
) => {
|
||||
api.getAutomationsManager().deleteAutomations(tag);
|
||||
|
||||
const shortcuts = api.getConfigManager().getConfig()?.view.keyboard_shortcuts;
|
||||
if (!shortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const automations = convertKeyboardShortcutsToAutomations(tag, shortcuts);
|
||||
if (automations.length) {
|
||||
api.getAutomationsManager().addAutomations(automations);
|
||||
}
|
||||
};
|
||||
|
||||
const ptzKeyboardShortcutToPTZAction = (
|
||||
ptzKbs: PTZKeyboardShortcutName,
|
||||
): PTZAction | null => {
|
||||
switch (ptzKbs) {
|
||||
case 'ptz_left':
|
||||
return 'left';
|
||||
case 'ptz_right':
|
||||
return 'right';
|
||||
case 'ptz_up':
|
||||
return 'up';
|
||||
case 'ptz_down':
|
||||
return 'down';
|
||||
case 'ptz_zoom_in':
|
||||
return 'zoom_in';
|
||||
case 'ptz_zoom_out':
|
||||
return 'zoom_out';
|
||||
}
|
||||
/* istanbul ignore next: No (current) way to reach this code -- @preserve */
|
||||
return null;
|
||||
};
|
||||
|
||||
const convertKeyboardShortcutsToAutomations = (
|
||||
tag: unknown,
|
||||
shortcuts: KeyboardShortcuts,
|
||||
): TaggedAutomations => {
|
||||
if (!shortcuts.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const automations: TaggedAutomations = [];
|
||||
|
||||
for (const name of [
|
||||
'ptz_down',
|
||||
'ptz_left',
|
||||
'ptz_right',
|
||||
'ptz_up',
|
||||
'ptz_zoom_in',
|
||||
'ptz_zoom_out',
|
||||
] as const) {
|
||||
const shortcut = shortcuts[name];
|
||||
const ptzAction = ptzKeyboardShortcutToPTZAction(name);
|
||||
if (!shortcut || !ptzAction) {
|
||||
continue;
|
||||
}
|
||||
|
||||
automations.push({
|
||||
conditions: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
key: shortcut.key,
|
||||
state: 'down',
|
||||
shift: shortcut.shift,
|
||||
ctrl: shortcut.ctrl,
|
||||
alt: shortcut.alt,
|
||||
meta: shortcut.meta,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
createPTZMultiAction({
|
||||
ptzAction: ptzAction,
|
||||
ptzPhase: 'start',
|
||||
}),
|
||||
],
|
||||
tag: tag,
|
||||
});
|
||||
|
||||
automations.push({
|
||||
conditions: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
key: shortcut.key,
|
||||
state: 'up',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
createPTZMultiAction({
|
||||
ptzAction: ptzAction,
|
||||
ptzPhase: 'stop',
|
||||
}),
|
||||
],
|
||||
tag: tag,
|
||||
});
|
||||
}
|
||||
|
||||
const homeShortcut = shortcuts.ptz_home;
|
||||
if (homeShortcut) {
|
||||
automations.push({
|
||||
conditions: [
|
||||
{
|
||||
condition: 'key' as const,
|
||||
key: homeShortcut.key,
|
||||
state: 'down',
|
||||
shift: homeShortcut.shift,
|
||||
ctrl: homeShortcut.ctrl,
|
||||
alt: homeShortcut.alt,
|
||||
meta: homeShortcut.meta,
|
||||
},
|
||||
],
|
||||
actions: [createPTZMultiAction()],
|
||||
tag: tag,
|
||||
});
|
||||
}
|
||||
|
||||
return automations;
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { FrigateCardConfig } from '../config/types';
|
||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import { EntityCache } from '../utils/ha/entity-registry/cache';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { ActionsManager } from './actions-manager';
|
||||
import { ActionsManager } from './actions/actions-manager';
|
||||
import { AutoUpdateManager } from './auto-update-manager';
|
||||
import { AutomationsManager } from './automations-manager';
|
||||
import { CameraURLManager } from './camera-url-manager';
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ScrollCallback,
|
||||
} from './card-element-manager';
|
||||
import { ConditionsManager, ConditionsManagerListener } from './conditions-manager';
|
||||
import { ConfigManager } from './config-manager';
|
||||
import { ConfigManager } from './config/config-manager';
|
||||
import { DownloadManager } from './download-manager';
|
||||
import { ExpandManager } from './expand-manager';
|
||||
import { FullscreenManager } from './fullscreen-manager';
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
CardHASSAPI,
|
||||
CardInitializerAPI,
|
||||
CardInteractionAPI,
|
||||
CardKeyboardStateAPI,
|
||||
CardMediaLoadedAPI,
|
||||
CardMediaPlayerAPI,
|
||||
CardMessageAPI,
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
CardViewAPI,
|
||||
} from './types';
|
||||
import { ViewManager } from './view-manager';
|
||||
import { KeyboardStateManager } from './keyboard-state-manager';
|
||||
|
||||
export class CardController
|
||||
implements
|
||||
@@ -72,6 +74,7 @@ export class CardController
|
||||
CardHASSAPI,
|
||||
CardInitializerAPI,
|
||||
CardInteractionAPI,
|
||||
CardKeyboardStateAPI,
|
||||
CardMediaLoadedAPI,
|
||||
CardMediaPlayerAPI,
|
||||
CardMessageAPI,
|
||||
@@ -101,6 +104,7 @@ export class CardController
|
||||
protected _hassManager = new HASSManager(this);
|
||||
protected _initializationManager = new InitializationManager(this);
|
||||
protected _interactionManager = new InteractionManager(this);
|
||||
protected _keyboardStateManager = new KeyboardStateManager(this);
|
||||
protected _mediaLoadedInfoManager = new MediaLoadedInfoManager(this);
|
||||
protected _mediaPlayerManager = new MediaPlayerManager(this);
|
||||
protected _messageManager = new MessageManager(this);
|
||||
@@ -195,6 +199,10 @@ export class CardController
|
||||
return this._interactionManager;
|
||||
}
|
||||
|
||||
public getKeyboardStateManager(): KeyboardStateManager {
|
||||
return this._keyboardStateManager;
|
||||
}
|
||||
|
||||
public getMediaLoadedInfoManager(): MediaLoadedInfoManager {
|
||||
return this._mediaLoadedInfoManager;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CardKeyboardStateAPI, KeysState } from './types';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
export class KeyboardStateManager {
|
||||
protected _api: CardKeyboardStateAPI;
|
||||
protected _state: KeysState = {};
|
||||
|
||||
constructor(api: CardKeyboardStateAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
const element = this._api.getCardElementManager().getElement();
|
||||
element.addEventListener('keydown', this._handleKeydown);
|
||||
element.addEventListener('keyup', this._handleKeyup);
|
||||
element.addEventListener('blur', this._handleBlur);
|
||||
}
|
||||
|
||||
public uninitialize(): void {
|
||||
const element = this._api.getCardElementManager().getElement();
|
||||
element.removeEventListener('keydown', this._handleKeydown);
|
||||
element.removeEventListener('keyup', this._handleKeyup);
|
||||
element.removeEventListener('blur', this._handleBlur);
|
||||
}
|
||||
|
||||
protected _handleKeydown = (ev: KeyboardEvent): void => {
|
||||
const keyObj = {
|
||||
state: 'down' as const,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
meta: ev.metaKey,
|
||||
shift: ev.shiftKey,
|
||||
};
|
||||
|
||||
if (!isEqual(this._state[ev.key], keyObj)) {
|
||||
this._state[ev.key] = keyObj;
|
||||
this._processStateChange();
|
||||
}
|
||||
};
|
||||
|
||||
protected _handleKeyup = (ev: KeyboardEvent): void => {
|
||||
if (ev.key in this._state && this._state[ev.key].state === 'down') {
|
||||
this._state[ev.key].state = 'up';
|
||||
this._processStateChange();
|
||||
}
|
||||
};
|
||||
|
||||
protected _handleBlur = (): void => {
|
||||
if (Object.keys(this._state).length) {
|
||||
// State is emptied if the element loses focus.
|
||||
this._state = {};
|
||||
this._processStateChange();
|
||||
}
|
||||
};
|
||||
|
||||
protected _processStateChange(): void {
|
||||
this._api.getConditionsManager().setState({ keys: this._state });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { FrigateCardCustomAction, FrigateCardViewAction } from '../config/types';
|
||||
import {
|
||||
createFrigateCardCameraAction,
|
||||
createFrigateCardSimpleAction
|
||||
} from '../utils/action.js';
|
||||
import { FrigateCardCustomAction, ViewActionConfig } from '../config/types';
|
||||
import { createCameraAction, createGeneralAction } from '../utils/action.js';
|
||||
import { CardQueryStringAPI } from './types';
|
||||
import { ViewManagerSetViewParameters } from './view-manager';
|
||||
|
||||
@@ -56,14 +53,15 @@ export class QueryStringManager {
|
||||
}
|
||||
|
||||
protected _executeNonViewRelated(intent: QueryStringViewIntent): void {
|
||||
// Only execute non-view actions when the card has rendered at least once.
|
||||
if (!this._api.getCardElementManager().hasUpdated()) {
|
||||
if (
|
||||
// Only execute non-view actions when the card has rendered at least once.
|
||||
!this._api.getCardElementManager().hasUpdated() ||
|
||||
!intent.other?.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
intent.other?.forEach((action) =>
|
||||
this._api.getActionsManager().executeFrigateAction(action),
|
||||
);
|
||||
this._api.getActionsManager().executeActions(intent.other);
|
||||
}
|
||||
|
||||
protected _calculateIntent(): QueryStringViewIntent {
|
||||
@@ -105,7 +103,7 @@ export class QueryStringManager {
|
||||
case 'camera_select':
|
||||
case 'live_substream_select':
|
||||
if (value) {
|
||||
customAction = createFrigateCardCameraAction(action, value, {
|
||||
customAction = createCameraAction(action, value, {
|
||||
cardID: cardID,
|
||||
});
|
||||
}
|
||||
@@ -125,7 +123,7 @@ export class QueryStringManager {
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
customAction = createFrigateCardSimpleAction(action, {
|
||||
customAction = createGeneralAction(action, {
|
||||
cardID: cardID,
|
||||
});
|
||||
break;
|
||||
@@ -143,7 +141,7 @@ export class QueryStringManager {
|
||||
|
||||
protected _isViewAction = (
|
||||
action: FrigateCardCustomAction,
|
||||
): action is FrigateCardViewAction => {
|
||||
): action is ViewActionConfig => {
|
||||
switch (action.frigate_card_action) {
|
||||
case 'clip':
|
||||
case 'clips':
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionsManager } from './conditions-manager';
|
||||
import type { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import type { ActionsManager } from './actions-manager';
|
||||
import type { ActionsManager } from './actions/actions-manager';
|
||||
import type { AutoUpdateManager } from './auto-update-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
import type { CameraURLManager } from './camera-url-manager';
|
||||
import type { CardElementManager } from './card-element-manager';
|
||||
import type { ConfigManager } from './config-manager';
|
||||
import type { ConfigManager } from './config/config-manager';
|
||||
import type { DownloadManager } from './download-manager';
|
||||
import type { ExpandManager } from './expand-manager';
|
||||
import type { FullscreenManager } from './fullscreen-manager';
|
||||
@@ -22,17 +22,22 @@ import type { StyleManager } from './style-manager';
|
||||
import type { TriggersManager } from './triggers-manager';
|
||||
import type { ViewManager } from './view-manager';
|
||||
import type { QueryStringManager } from './query-string-manager';
|
||||
import { KeyboardStateManager } from './keyboard-state-manager';
|
||||
import { Automation } from '../config/types';
|
||||
|
||||
/**
|
||||
* This defines a series of limited APIs that various manager helpers use to
|
||||
* control the card. Explicitly specifying them helps make coupling intentional
|
||||
* and avoids cyclic importing.
|
||||
*/
|
||||
// *************************************************************************
|
||||
// Manager APIs
|
||||
// This defines a series of limited APIs that various managers use to control
|
||||
// the card. Explicitly specifying them helps make coupling intentional and
|
||||
// reduce cyclic dependencies.
|
||||
// *************************************************************************
|
||||
|
||||
export interface CardActionsManagerAPI {
|
||||
export interface CardActionsAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getCameraManager(): CameraManager;
|
||||
getCameraURLManager(): CameraURLManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getDownloadManager(): DownloadManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
@@ -45,11 +50,12 @@ export interface CardActionsManagerAPI {
|
||||
getTriggersManager(): TriggersManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
export type CardActionsManagerAPI = CardActionsAPI;
|
||||
|
||||
export interface CardAutomationsAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getMessageManager(): MessageManager;
|
||||
}
|
||||
@@ -62,6 +68,7 @@ export interface CardAutoRefreshAPI {
|
||||
}
|
||||
|
||||
export interface CardCameraAPI {
|
||||
getActionsManager(): ActionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getEntityRegistryManager(): EntityRegistryManager;
|
||||
getHASSManager(): HASSManager;
|
||||
@@ -84,6 +91,7 @@ export interface CardConfigAPI {
|
||||
getAutomationsManager(): AutomationsManager;
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getInitializationManager(): InitializationManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMessageManager(): MessageManager;
|
||||
@@ -91,6 +99,11 @@ export interface CardConfigAPI {
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
export interface CardConfigLoaderAPI {
|
||||
getConfigManager(): ConfigManager;
|
||||
getAutomationsManager(): AutomationsManager;
|
||||
}
|
||||
|
||||
export interface CardDownloadAPI {
|
||||
getCameraManager(): CameraManager;
|
||||
getHASSManager(): HASSManager;
|
||||
@@ -106,6 +119,7 @@ export interface CardElementAPI {
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getInitializationManager(): InitializationManager;
|
||||
getInteractionManager(): InteractionManager;
|
||||
getKeyboardStateManager(): KeyboardStateManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
@@ -159,6 +173,12 @@ export interface CardInteractionAPI {
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
|
||||
export interface CardKeyboardStateAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
}
|
||||
|
||||
export interface CardMediaLoadedAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
@@ -224,3 +244,21 @@ export interface CardViewAPI {
|
||||
getStyleManager(): StyleManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
// Common Types
|
||||
// *************************************************************************
|
||||
|
||||
export interface KeysState {
|
||||
[key: string]: {
|
||||
state: 'down' | 'up';
|
||||
ctrl: boolean;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
meta: boolean;
|
||||
}
|
||||
}
|
||||
interface TaggedAutomation extends Automation {
|
||||
tag?: unknown;
|
||||
}
|
||||
export type TaggedAutomations = TaggedAutomation[];
|
||||
|
||||
Reference in New Issue
Block a user