Add initial keyboard shortcut support.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { LitElement, ReactiveController } from 'lit';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { KeyboardShortcut } from '../config/keyboard-shortcuts';
|
||||
import { setOrRemoveAttribute } from '../utils/basic';
|
||||
|
||||
export class KeyAssignerController implements ReactiveController {
|
||||
protected _host: LitElement;
|
||||
protected _assigning = false;
|
||||
protected _value: KeyboardShortcut | null = null;
|
||||
|
||||
constructor(host: LitElement) {
|
||||
this._host = host;
|
||||
this._host.addController(this);
|
||||
}
|
||||
|
||||
public setValue(value: KeyboardShortcut | null): void {
|
||||
if (!isEqual(value, this._value)) {
|
||||
this._value = value;
|
||||
this._host.requestUpdate();
|
||||
|
||||
this._host.dispatchEvent(
|
||||
new CustomEvent('value-changed', {
|
||||
detail: {
|
||||
value: this._value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
public getValue(): KeyboardShortcut | null {
|
||||
return this._value;
|
||||
}
|
||||
public hasValue(): boolean {
|
||||
return !!this._value;
|
||||
}
|
||||
|
||||
public isAssigning(): boolean {
|
||||
return this._assigning;
|
||||
}
|
||||
public toggleAssigning(): void {
|
||||
this._setAssigning(!this._assigning);
|
||||
}
|
||||
protected _setAssigning(assigning: boolean): void {
|
||||
this._assigning = assigning;
|
||||
setOrRemoveAttribute(this._host, this._assigning, 'assigning');
|
||||
|
||||
if (this._assigning) {
|
||||
this._host.addEventListener('keydown', this._keydownEventHandler);
|
||||
} else {
|
||||
this._host.removeEventListener('keydown', this._keydownEventHandler);
|
||||
}
|
||||
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
protected _blurEventHandler = (): void => {
|
||||
this._setAssigning(false);
|
||||
};
|
||||
|
||||
protected _keydownEventHandler = (ev: KeyboardEvent): void => {
|
||||
// Don't allow _only_ a modifier.
|
||||
if (!ev.key || ['Control', 'Alt', 'Shift', 'Meta'].includes(ev.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setValue({
|
||||
key: ev.key,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey,
|
||||
meta: ev.metaKey,
|
||||
});
|
||||
this._setAssigning(false);
|
||||
};
|
||||
|
||||
public hostConnected(): void {
|
||||
this._host.addEventListener('blur', this._blurEventHandler);
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
this._host.removeEventListener('blur', this._blurEventHandler);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ interface LiveViewContext {
|
||||
// camera to be live rather than the camera selected in the view).
|
||||
overrides?: Map<string, string>;
|
||||
|
||||
ptzVisible?: boolean;
|
||||
fetchThumbnails?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,17 @@ import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { MediaLoadedInfo } from '../types';
|
||||
import {
|
||||
createFrigateCardCameraAction,
|
||||
createFrigateCardChangeZoomAction,
|
||||
createFrigateCardDisplayModeAction,
|
||||
createFrigateCardMediaPlayerAction,
|
||||
createFrigateCardShowPTZAction,
|
||||
createFrigateCardSimpleAction,
|
||||
createCameraAction,
|
||||
createPTZMultiAction,
|
||||
createDisplayModeAction,
|
||||
createMediaPlayerAction,
|
||||
createPTZControlsAction,
|
||||
createGeneralAction,
|
||||
} from '../utils/action';
|
||||
import { isTruthy } from '../utils/basic';
|
||||
import { getEntityIcon, getEntityTitle } from '../utils/ha';
|
||||
import { hasUsablePTZ } from '../utils/ptz';
|
||||
import { hasSubstream } from '../utils/substream';
|
||||
import { getPTZTarget } from '../utils/ptz';
|
||||
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
||||
|
||||
@@ -95,8 +95,8 @@ export class MenuButtonController {
|
||||
this._getMuteUnmuteButton(config, options?.currentMediaLoadedInfo),
|
||||
this._getScreenshotButton(config, options?.currentMediaLoadedInfo),
|
||||
this._getDisplayModeButton(config, cameraManager, view),
|
||||
this._getPTZButton(config, cameraManager, view),
|
||||
this._getDefaultZoomButton(config, view),
|
||||
this._getPTZControlsButton(config, cameraManager, view),
|
||||
this._getPTZHomeButton(config, cameraManager, view),
|
||||
|
||||
...this._dynamicMenuButtons.map((button) => ({
|
||||
style: this._getStyleFromActions(config, view, button, options),
|
||||
@@ -115,11 +115,9 @@ export class MenuButtonController {
|
||||
title: localize('config.menu.buttons.frigate'),
|
||||
tap_action:
|
||||
config.menu?.style === 'hidden'
|
||||
? (createFrigateCardSimpleAction('menu_toggle') as FrigateCardCustomAction)
|
||||
: (createFrigateCardSimpleAction('default') as FrigateCardCustomAction),
|
||||
hold_action: createFrigateCardSimpleAction(
|
||||
'diagnostics',
|
||||
) as FrigateCardCustomAction,
|
||||
? (createGeneralAction('menu_toggle') as FrigateCardCustomAction)
|
||||
: (createGeneralAction('default') as FrigateCardCustomAction),
|
||||
hold_action: createGeneralAction('diagnostics') as FrigateCardCustomAction,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,7 +133,7 @@ export class MenuButtonController {
|
||||
const menuItems = Array.from(
|
||||
cameraManager.getStore().getCameraConfigEntries(menuCameraIDs),
|
||||
([cameraID, config]) => {
|
||||
const action = createFrigateCardCameraAction('camera_select', cameraID);
|
||||
const action = createCameraAction('camera_select', cameraID);
|
||||
const metadata = cameraManager.getCameraMetadata(cameraID);
|
||||
|
||||
return {
|
||||
@@ -175,7 +173,7 @@ export class MenuButtonController {
|
||||
(cameraID) => cameraID !== view.camera,
|
||||
);
|
||||
const streams = [view.camera, ...substreams];
|
||||
const substreamAwareCameraID = this._getSubstreamAwareCameraID(view);
|
||||
const substreamAwareCameraID = getStreamCameraID(view);
|
||||
|
||||
if (streams.length === 2) {
|
||||
// If there are only two dependencies (the main camera, and 1 other)
|
||||
@@ -187,16 +185,13 @@ export class MenuButtonController {
|
||||
title: localize('config.menu.buttons.substreams'),
|
||||
...config.menu.buttons.substreams,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
tap_action: createGeneralAction(
|
||||
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||
) as FrigateCardCustomAction,
|
||||
};
|
||||
} else if (streams.length > 2) {
|
||||
const menuItems = Array.from(streams, (streamID) => {
|
||||
const action = createFrigateCardCameraAction(
|
||||
'live_substream_select',
|
||||
streamID,
|
||||
);
|
||||
const action = createCameraAction('live_substream_select', streamID);
|
||||
const metadata = cameraManager.getCameraMetadata(streamID) ?? undefined;
|
||||
const cameraConfig = cameraManager.getStore().getCameraConfig(streamID);
|
||||
return {
|
||||
@@ -236,7 +231,7 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.live'),
|
||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction('live') as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('live') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -253,8 +248,8 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.clips'),
|
||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction('clips') as FrigateCardCustomAction,
|
||||
hold_action: createFrigateCardSimpleAction('clip') as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('clips') as FrigateCardCustomAction,
|
||||
hold_action: createGeneralAction('clip') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -271,12 +266,8 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.snapshots'),
|
||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'snapshots',
|
||||
) as FrigateCardCustomAction,
|
||||
hold_action: createFrigateCardSimpleAction(
|
||||
'snapshot',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('snapshots') as FrigateCardCustomAction,
|
||||
hold_action: createGeneralAction('snapshot') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -293,12 +284,8 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.recordings'),
|
||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'recordings',
|
||||
) as FrigateCardCustomAction,
|
||||
hold_action: createFrigateCardSimpleAction(
|
||||
'recording',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('recordings') as FrigateCardCustomAction,
|
||||
hold_action: createGeneralAction('recording') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -315,7 +302,7 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.image'),
|
||||
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction('image') as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('image') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -332,9 +319,7 @@ export class MenuButtonController {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.view.views.timeline'),
|
||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'timeline',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('timeline') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -358,7 +343,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.download,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.download'),
|
||||
tap_action: createFrigateCardSimpleAction('download') as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('download') as FrigateCardCustomAction,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -374,9 +359,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.camera_ui,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.camera_ui'),
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'camera_ui',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('camera_ui') as FrigateCardCustomAction,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -402,16 +385,16 @@ export class MenuButtonController {
|
||||
style: forbidden || muted ? {} : this._getEmphasizedStyle(true),
|
||||
...(!forbidden &&
|
||||
buttonType === 'momentary' && {
|
||||
start_tap_action: createFrigateCardSimpleAction(
|
||||
start_tap_action: createGeneralAction(
|
||||
'microphone_unmute',
|
||||
) as FrigateCardCustomAction,
|
||||
end_tap_action: createFrigateCardSimpleAction(
|
||||
end_tap_action: createGeneralAction(
|
||||
'microphone_mute',
|
||||
) as FrigateCardCustomAction,
|
||||
}),
|
||||
...(!forbidden &&
|
||||
buttonType === 'toggle' && {
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
tap_action: createGeneralAction(
|
||||
muted ? 'microphone_unmute' : 'microphone_mute',
|
||||
) as FrigateCardCustomAction,
|
||||
}),
|
||||
@@ -429,7 +412,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.expand,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.expand'),
|
||||
tap_action: createFrigateCardSimpleAction('expand') as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('expand') as FrigateCardCustomAction,
|
||||
style: inExpandedMode ? this._getEmphasizedStyle() : {},
|
||||
};
|
||||
}
|
||||
@@ -444,9 +427,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.fullscreen,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.fullscreen'),
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'fullscreen',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('fullscreen') as FrigateCardCustomAction,
|
||||
style: inFullscreenMode ? this._getEmphasizedStyle() : {},
|
||||
}
|
||||
: null;
|
||||
@@ -469,8 +450,8 @@ export class MenuButtonController {
|
||||
.map((playerEntityID) => {
|
||||
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
||||
const state = hass.states[playerEntityID];
|
||||
const playAction = createFrigateCardMediaPlayerAction(playerEntityID, 'play');
|
||||
const stopAction = createFrigateCardMediaPlayerAction(playerEntityID, 'stop');
|
||||
const playAction = createMediaPlayerAction(playerEntityID, 'play');
|
||||
const stopAction = createMediaPlayerAction(playerEntityID, 'stop');
|
||||
const disabled = !state || state.state === 'unavailable';
|
||||
|
||||
return {
|
||||
@@ -512,7 +493,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.play,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.play'),
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
tap_action: createGeneralAction(
|
||||
paused ? 'play' : 'pause',
|
||||
) as FrigateCardCustomAction,
|
||||
};
|
||||
@@ -535,7 +516,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.mute,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.mute'),
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
tap_action: createGeneralAction(
|
||||
muted ? 'unmute' : 'mute',
|
||||
) as FrigateCardCustomAction,
|
||||
};
|
||||
@@ -553,9 +534,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.screenshot,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.screenshot'),
|
||||
tap_action: createFrigateCardSimpleAction(
|
||||
'screenshot',
|
||||
) as FrigateCardCustomAction,
|
||||
tap_action: createGeneralAction('screenshot') as FrigateCardCustomAction,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -577,65 +556,75 @@ export class MenuButtonController {
|
||||
title: isGrid
|
||||
? localize('display_modes.single')
|
||||
: localize('display_modes.grid'),
|
||||
tap_action: createFrigateCardDisplayModeAction(isGrid ? 'single' : 'grid'),
|
||||
tap_action: createDisplayModeAction(isGrid ? 'single' : 'grid'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _getSubstreamAwareCameraID(view: View): string {
|
||||
return view.is('live')
|
||||
? view.context?.live?.overrides?.get(view.camera) ?? view.camera
|
||||
: view.camera;
|
||||
}
|
||||
|
||||
protected _getPTZButton(
|
||||
protected _getPTZControlsButton(
|
||||
config: FrigateCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view: View,
|
||||
): MenuItem | null {
|
||||
const substreamAwareCameraCapabilities = cameraManager.getCameraCapabilities(
|
||||
this._getSubstreamAwareCameraID(view),
|
||||
);
|
||||
const ptzConfig = view.is('live')
|
||||
? config.live.controls.ptz
|
||||
: view.isViewerView()
|
||||
? config.media_viewer.controls.ptz
|
||||
: null;
|
||||
|
||||
if (
|
||||
view.is('live') &&
|
||||
hasUsablePTZ(substreamAwareCameraCapabilities, config.live.controls.ptz)
|
||||
) {
|
||||
if (!ptzConfig || ptzConfig.mode === 'off') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ptzTarget = getPTZTarget(view, {
|
||||
cameraManager: cameraManager,
|
||||
...(ptzConfig.mode === 'auto' && { type: 'ptz' }),
|
||||
});
|
||||
|
||||
if (ptzTarget) {
|
||||
const isOn =
|
||||
view.context?.live?.ptzVisible === false
|
||||
? false
|
||||
: config.live.controls.ptz.mode === 'on';
|
||||
view.context?.ptzControls?.enabled !== false &&
|
||||
(ptzConfig.mode === 'on' ||
|
||||
(ptzConfig.mode === 'auto' && ptzTarget.type === 'ptz'));
|
||||
return {
|
||||
icon: 'mdi:pan',
|
||||
...config.menu.buttons.ptz,
|
||||
...config.menu.buttons.ptz_controls,
|
||||
style: isOn ? this._getEmphasizedStyle() : {},
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.ptz'),
|
||||
tap_action: createFrigateCardShowPTZAction(!isOn),
|
||||
title: localize('config.menu.buttons.ptz_controls'),
|
||||
tap_action: createPTZControlsAction(!isOn),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _getDefaultZoomButton(
|
||||
protected _getPTZHomeButton(
|
||||
config: FrigateCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view: View,
|
||||
): MenuItem | null {
|
||||
const targetID = view.isViewerView()
|
||||
? view.queryResults?.getSelectedResult()?.getID() ?? null
|
||||
: this._getSubstreamAwareCameraID(view);
|
||||
const target = getPTZTarget(view, {
|
||||
cameraManager: cameraManager,
|
||||
});
|
||||
|
||||
if (!targetID || (view.context?.zoom?.[targetID]?.isDefault ?? true)) {
|
||||
if (
|
||||
!target ||
|
||||
((target.type === 'digital' &&
|
||||
view.context?.zoom?.[target.targetID]?.observed?.isDefault) ??
|
||||
true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
icon: 'mdi:magnify-close',
|
||||
...config.menu.buttons.default_zoom,
|
||||
icon: 'mdi:home',
|
||||
...config.menu.buttons.ptz_home,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.default_zoom'),
|
||||
tap_action: createFrigateCardChangeZoomAction(targetID) as FrigateCardCustomAction,
|
||||
title: localize('config.menu.buttons.ptz_home'),
|
||||
tap_action: createPTZMultiAction({
|
||||
targetID: target.targetID,
|
||||
}) as FrigateCardCustomAction,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ import type {
|
||||
import { FRIGATE_BUTTON_MENU_ICON } from '../const.js';
|
||||
import { StateParameters } from '../types.js';
|
||||
import {
|
||||
convertActionToFrigateCardCustomAction,
|
||||
frigateCardHandleActionConfig,
|
||||
convertActionToCardCustomAction,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action';
|
||||
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
||||
import { refreshDynamicStateParameters } from '../utils/ha/index.js';
|
||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
||||
|
||||
export class MenuController {
|
||||
protected _host: LitElement;
|
||||
@@ -95,7 +95,6 @@ export class MenuController {
|
||||
}
|
||||
|
||||
public actionHandler(
|
||||
hass: HomeAssistant,
|
||||
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
|
||||
config?: ActionsConfig,
|
||||
): void {
|
||||
@@ -134,7 +133,10 @@ export class MenuController {
|
||||
}
|
||||
|
||||
if (toggleLessActions.length) {
|
||||
frigateCardHandleActionConfig(this._host, hass, config, interaction, actions);
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: actions,
|
||||
config: config,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isHidingMenu()) {
|
||||
@@ -209,7 +211,7 @@ export class MenuController {
|
||||
}
|
||||
|
||||
protected _isMenuToggleAction(action: ActionType): boolean {
|
||||
const frigateCardAction = convertActionToFrigateCardCustomAction(action);
|
||||
const frigateCardAction = convertActionToCardCustomAction(action);
|
||||
return !!frigateCardAction && frigateCardAction.frigate_card_action == 'menu_toggle';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import {
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
FrigateCardPTZAction,
|
||||
FrigateCardPTZActions,
|
||||
FrigateCardPTZConfig,
|
||||
PTZAction,
|
||||
PTZControlAction,
|
||||
PTZ_CONTROL_ACTIONS,
|
||||
} from '../config/types';
|
||||
import {
|
||||
frigateCardHandleActionConfig,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action';
|
||||
|
||||
export class PTZController {
|
||||
private _host: HTMLElement;
|
||||
|
||||
private _config: FrigateCardPTZConfig | null = null;
|
||||
private _hass: HomeAssistant | null = null;
|
||||
private _cameraManager: CameraManager | null = null;
|
||||
private _cameraID: string | null = null;
|
||||
private _actions: FrigateCardPTZActions | null = null;
|
||||
private _forceVisibility?: boolean;
|
||||
|
||||
constructor(host: HTMLElement) {
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
public setConfig(config?: FrigateCardPTZConfig) {
|
||||
this._config = config ?? null;
|
||||
|
||||
this._host.setAttribute('data-orientation', config?.orientation ?? 'horizontal');
|
||||
this._host.setAttribute('data-position', config?.position ?? 'bottom-right');
|
||||
this._host.setAttribute(
|
||||
'style',
|
||||
Object.entries(config?.style ?? {})
|
||||
.map(([k, v]) => `${k}:${v}`)
|
||||
.join(';'),
|
||||
);
|
||||
}
|
||||
|
||||
public getConfig(): FrigateCardPTZConfig | null {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public setHASS(hass?: HomeAssistant) {
|
||||
this._hass = hass ?? null;
|
||||
}
|
||||
|
||||
public setCamera(cameraManager?: CameraManager, cameraID?: string) {
|
||||
this._cameraManager = cameraManager ?? null;
|
||||
this._cameraID = cameraID ?? null;
|
||||
|
||||
this._calculateActions();
|
||||
}
|
||||
|
||||
public setForceVisibility(forceVisibility?: boolean): void {
|
||||
this._forceVisibility = forceVisibility;
|
||||
}
|
||||
|
||||
public handleAction(
|
||||
ev: HASSDomEvent<{ action: string }>,
|
||||
config?: ActionsConfig | null,
|
||||
): void {
|
||||
// Nothing else has the configuration for this action, so don't let it
|
||||
// propagate further.
|
||||
ev.stopPropagation();
|
||||
|
||||
const interaction: string = ev.detail.action;
|
||||
const action = getActionConfigGivenAction(interaction, config);
|
||||
if (config && action && this._hass) {
|
||||
frigateCardHandleActionConfig(this._host, this._hass, config, interaction, action);
|
||||
}
|
||||
}
|
||||
|
||||
public getPTZActions(actionName: PTZControlAction): Actions | null {
|
||||
const propertyName = 'actions_' + actionName;
|
||||
return this._config?.[propertyName] ?? this._actions?.[propertyName] ?? null;
|
||||
}
|
||||
|
||||
private _hasAnyAction(): boolean {
|
||||
for (const actionName of PTZ_CONTROL_ACTIONS) {
|
||||
if ('actions_' + actionName in (this._actions ?? {})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public shouldDisplay(): boolean {
|
||||
return this._forceVisibility === false
|
||||
? false
|
||||
: this._config?.mode === 'on' && this._hasAnyAction();
|
||||
}
|
||||
|
||||
private _calculateActions(): void {
|
||||
const getDefaultAction = (
|
||||
ptzAction: PTZAction,
|
||||
options?: {
|
||||
phase?: 'start' | 'stop';
|
||||
preset?: string;
|
||||
},
|
||||
): FrigateCardPTZAction => ({
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'ptz',
|
||||
ptz_action: ptzAction,
|
||||
...(options?.phase && { ptz_phase: options.phase }),
|
||||
...(options?.preset && { ptz_preset: options.preset }),
|
||||
});
|
||||
|
||||
const getDefaultActions = (
|
||||
ptzAction: PTZAction,
|
||||
continuous: boolean,
|
||||
preset?: string,
|
||||
): Actions =>
|
||||
continuous
|
||||
? {
|
||||
start_tap_action: getDefaultAction(ptzAction, {
|
||||
phase: 'start',
|
||||
preset: preset,
|
||||
}),
|
||||
end_tap_action: getDefaultAction(ptzAction, {
|
||||
phase: 'stop',
|
||||
preset: preset,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
tap_action: getDefaultAction(ptzAction, { preset: preset }),
|
||||
};
|
||||
|
||||
if (!this._cameraManager || !this._cameraID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ptzCapabilities = this._cameraManager.getCameraCapabilities(
|
||||
this._cameraID,
|
||||
)?.getPTZCapabilities();
|
||||
|
||||
const defaultActions: FrigateCardPTZActions = {};
|
||||
const panTilt = ptzCapabilities?.panTilt;
|
||||
const zoom = ptzCapabilities?.zoom;
|
||||
const presets = ptzCapabilities?.presets;
|
||||
|
||||
if (panTilt?.length) {
|
||||
const continuous = panTilt.includes('continuous');
|
||||
defaultActions.actions_up = getDefaultActions('up', continuous);
|
||||
defaultActions.actions_down = getDefaultActions('down', continuous);
|
||||
defaultActions.actions_left = getDefaultActions('left', continuous);
|
||||
defaultActions.actions_right = getDefaultActions('right', continuous);
|
||||
}
|
||||
|
||||
if (zoom?.length) {
|
||||
const continuous = zoom.includes('continuous');
|
||||
defaultActions.actions_zoom_in = getDefaultActions('zoom_in', continuous);
|
||||
defaultActions.actions_zoom_out = getDefaultActions('zoom_out', continuous);
|
||||
}
|
||||
|
||||
if (presets?.length) {
|
||||
defaultActions.actions_home = getDefaultActions('preset', false, presets[0]);
|
||||
}
|
||||
|
||||
this._actions = {
|
||||
...defaultActions,
|
||||
...this._config,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { CameraManager } from '../../camera-manager/manager';
|
||||
import { dispatchActionExecutionRequest } from '../../card-controller/actions/utils/execution-request';
|
||||
import { PTZAction } from '../../config/ptz';
|
||||
import { Actions, ActionsConfig, PTZControlsConfig } from '../../config/types';
|
||||
import { createPTZMultiAction, getActionConfigGivenAction } from '../../utils/action';
|
||||
import { PTZActionNameToMultiAction, PTZActionPresence } from './types';
|
||||
|
||||
export class PTZController {
|
||||
private _host: HTMLElement;
|
||||
|
||||
private _config: PTZControlsConfig | null = null;
|
||||
private _hass: HomeAssistant | null = null;
|
||||
private _cameraManager: CameraManager | null = null;
|
||||
private _cameraID: string | null = null;
|
||||
|
||||
private _forceVisibility?: boolean;
|
||||
|
||||
constructor(host: HTMLElement) {
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
public setConfig(config?: PTZControlsConfig) {
|
||||
this._config = config ?? null;
|
||||
|
||||
this._host.setAttribute('data-orientation', config?.orientation ?? 'horizontal');
|
||||
this._host.setAttribute('data-position', config?.position ?? 'bottom-right');
|
||||
this._host.setAttribute(
|
||||
'style',
|
||||
Object.entries(config?.style ?? {})
|
||||
.map(([k, v]) => `${k}:${v}`)
|
||||
.join(';'),
|
||||
);
|
||||
}
|
||||
|
||||
public getConfig(): PTZControlsConfig | null {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
public setCamera(cameraManager?: CameraManager, cameraID?: string): void {
|
||||
this._cameraManager = cameraManager ?? null;
|
||||
this._cameraID = cameraID ?? null;
|
||||
}
|
||||
|
||||
public setForceVisibility(forceVisibility?: boolean): void {
|
||||
this._forceVisibility = forceVisibility;
|
||||
}
|
||||
|
||||
public handleAction(
|
||||
ev: HASSDomEvent<{ action: string }>,
|
||||
config?: ActionsConfig | null,
|
||||
): void {
|
||||
// Nothing else has the configuration for this action, so don't let it
|
||||
// propagate further.
|
||||
ev.stopPropagation();
|
||||
|
||||
const interaction: string = ev.detail.action;
|
||||
const action = getActionConfigGivenAction(interaction, config);
|
||||
if (action) {
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
action: action,
|
||||
...(config && { config: config }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public hasUsefulAction(): PTZActionPresence {
|
||||
const allUsefulActions = {
|
||||
pt: true,
|
||||
z: true,
|
||||
home: true,
|
||||
};
|
||||
if (!this._cameraID) {
|
||||
// Will use digital PTZ.
|
||||
return allUsefulActions;
|
||||
}
|
||||
const capabilities = this._cameraManager?.getCameraCapabilities(this._cameraID);
|
||||
if (!capabilities || !capabilities.hasPTZCapability()) {
|
||||
// Will use digital PTZ.
|
||||
return allUsefulActions;
|
||||
}
|
||||
|
||||
const ptzCapabilities = capabilities.getPTZCapabilities();
|
||||
return {
|
||||
pt:
|
||||
!!ptzCapabilities?.up ||
|
||||
!!ptzCapabilities?.down ||
|
||||
!!ptzCapabilities?.left ||
|
||||
!!ptzCapabilities?.right,
|
||||
z: !!ptzCapabilities?.zoomIn || !!ptzCapabilities?.zoomOut,
|
||||
home: !!ptzCapabilities?.presets?.length,
|
||||
};
|
||||
}
|
||||
|
||||
public shouldDisplay(): boolean {
|
||||
return this._forceVisibility !== undefined
|
||||
? this._forceVisibility
|
||||
: this._config?.mode === 'auto'
|
||||
? !!this._cameraID &&
|
||||
!!this._cameraManager?.getCameraCapabilities(this._cameraID)?.hasPTZCapability()
|
||||
: this._config?.mode === 'on';
|
||||
}
|
||||
|
||||
public getPTZActions(): PTZActionNameToMultiAction {
|
||||
const getDefaultActions = (options?: {
|
||||
ptzAction?: PTZAction;
|
||||
preset?: string;
|
||||
}): Actions => ({
|
||||
start_tap_action: createPTZMultiAction({
|
||||
ptzAction: options?.ptzAction,
|
||||
ptzPhase: 'start',
|
||||
ptzPreset: options?.preset,
|
||||
}),
|
||||
end_tap_action: createPTZMultiAction({
|
||||
ptzAction: options?.ptzAction,
|
||||
ptzPhase: 'stop',
|
||||
ptzPreset: options?.preset,
|
||||
}),
|
||||
});
|
||||
|
||||
const actions: PTZActionNameToMultiAction = {};
|
||||
actions.up = getDefaultActions({
|
||||
ptzAction: 'up',
|
||||
});
|
||||
actions.down = getDefaultActions({
|
||||
ptzAction: 'down',
|
||||
});
|
||||
actions.left = getDefaultActions({
|
||||
ptzAction: 'left',
|
||||
});
|
||||
actions.right = getDefaultActions({
|
||||
ptzAction: 'right',
|
||||
});
|
||||
actions.zoom_in = getDefaultActions({
|
||||
ptzAction: 'zoom_in',
|
||||
});
|
||||
actions.zoom_out = getDefaultActions({
|
||||
ptzAction: 'zoom_out',
|
||||
});
|
||||
actions.home = {
|
||||
tap_action: createPTZMultiAction(),
|
||||
};
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { PTZControlAction } from '../../config/ptz';
|
||||
import { Actions } from '../../config/types';
|
||||
|
||||
interface PTZControlsViewContext {
|
||||
enabled?: boolean;
|
||||
}
|
||||
declare module 'view' {
|
||||
interface ViewContext {
|
||||
ptzControls?: PTZControlsViewContext;
|
||||
}
|
||||
}
|
||||
|
||||
export type PTZActionNameToMultiAction = {
|
||||
[K in PTZControlAction]?: Actions;
|
||||
};
|
||||
|
||||
export interface PTZActionPresence {
|
||||
pt: boolean;
|
||||
z: boolean;
|
||||
home: boolean;
|
||||
}
|
||||
@@ -1,11 +1,29 @@
|
||||
export interface ZoomConfig {
|
||||
pan?: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
import { PartialDeep } from 'type-fest';
|
||||
|
||||
export const ZOOM_DEFAULT_PAN_X = 50;
|
||||
export const ZOOM_DEFAULT_PAN_Y = 50;
|
||||
export const ZOOM_DEFAULT_SCALE = 1;
|
||||
export const ZOOM_PRECISION = 4;
|
||||
|
||||
export interface ZoomSettingsBase {
|
||||
pan: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
zoom?: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export interface ZoomDefault {
|
||||
export type PartialZoomSettings = PartialDeep<ZoomSettingsBase>;
|
||||
|
||||
export interface ZoomSettingsObserved extends ZoomSettingsBase {
|
||||
isDefault: boolean;
|
||||
unzoomed: boolean;
|
||||
}
|
||||
|
||||
export const isZoomEmpty = (settings?: PartialZoomSettings | null): boolean => {
|
||||
return (
|
||||
settings?.pan?.x === undefined &&
|
||||
settings?.pan?.y === undefined &&
|
||||
settings?.zoom === undefined
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import Panzoom, { PanzoomEventDetail, PanzoomObject } from '@dermotduffy/panzoom';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import round from 'lodash-es/round';
|
||||
import {
|
||||
arefloatsApproximatelyEqual,
|
||||
dispatchFrigateCardEvent,
|
||||
isHoverableDevice,
|
||||
} from '../../utils/basic';
|
||||
import { ZoomConfig } from './types';
|
||||
|
||||
const ZOOM_DEFAULT_PAN_X = 50;
|
||||
const ZOOM_DEFAULT_PAN_Y = 50;
|
||||
const ZOOM_DEFAULT_SCALE = 1;
|
||||
const ZOOM_PRECISION = 4;
|
||||
import {
|
||||
PartialZoomSettings,
|
||||
ZOOM_DEFAULT_PAN_X,
|
||||
ZOOM_DEFAULT_PAN_Y,
|
||||
ZOOM_DEFAULT_SCALE,
|
||||
ZOOM_PRECISION,
|
||||
ZoomSettingsObserved,
|
||||
isZoomEmpty,
|
||||
} from './types';
|
||||
|
||||
export class ZoomController {
|
||||
protected _element: HTMLElement;
|
||||
@@ -26,15 +29,14 @@ export class ZoomController {
|
||||
// Should clicks be allowed to propagate, or consumed as a pan/zoom action?
|
||||
protected _allowClick = true;
|
||||
|
||||
protected _defaultConfig: ZoomConfig | null;
|
||||
protected _config: ZoomConfig | null;
|
||||
protected _defaultSettings: PartialZoomSettings | null;
|
||||
protected _settings: PartialZoomSettings | null;
|
||||
|
||||
// When the user pans/zooms changes may be created at a very high rate.
|
||||
protected _debouncedChangeHandler = debounce(this._changeHandler.bind(this), 200);
|
||||
|
||||
// Multiple calls to setConfig() or setDefaultConfig() or resizes should only
|
||||
// update once.
|
||||
protected _debouncedUpdater = debounce(this._updateBasedOnConfig.bind(this), 200);
|
||||
// These values should be suitably less than the value of STEP_DELAY_SECONDS
|
||||
// in the ptz-digital action, in order to ensure smooth movements of the
|
||||
// digital PTZ actions.
|
||||
protected _debouncedChangeHandler = throttle(this._changeHandler.bind(this), 50);
|
||||
protected _debouncedUpdater = throttle(this._updateBasedOnConfig.bind(this), 50);
|
||||
|
||||
protected _resizeObserver = new ResizeObserver(this._debouncedUpdater);
|
||||
|
||||
@@ -70,6 +72,11 @@ export class ZoomController {
|
||||
// handler in the viewer).
|
||||
if (!this._allowClick) {
|
||||
ev.stopPropagation();
|
||||
|
||||
// Even though the click is stopped,the card still needs to gain focus so
|
||||
// that keyboard shortcuts will work immediately after the card is clicked
|
||||
// upon.
|
||||
dispatchFrigateCardEvent(this._element, 'focus');
|
||||
}
|
||||
this._allowClick = true;
|
||||
};
|
||||
@@ -97,11 +104,14 @@ export class ZoomController {
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
options?: { config?: ZoomConfig | null; defaultConfig?: ZoomConfig | null },
|
||||
options?: {
|
||||
config?: PartialZoomSettings | null;
|
||||
defaultConfig?: PartialZoomSettings | null;
|
||||
},
|
||||
) {
|
||||
this._element = element;
|
||||
this._config = options?.config ?? null;
|
||||
this._defaultConfig = options?.defaultConfig ?? null;
|
||||
this._settings = options?.config ?? null;
|
||||
this._defaultSettings = options?.defaultConfig ?? null;
|
||||
}
|
||||
|
||||
public activate(): void {
|
||||
@@ -182,44 +192,47 @@ export class ZoomController {
|
||||
this._element.removeEventListener('panzoomchange', this._debouncedChangeHandler);
|
||||
}
|
||||
|
||||
public setDefaultConfig(config: ZoomConfig | null): void {
|
||||
this._defaultConfig = config;
|
||||
public setDefaultSettings(config: PartialZoomSettings | null): void {
|
||||
this._defaultSettings = config;
|
||||
this._debouncedUpdater();
|
||||
}
|
||||
|
||||
public setConfig(config: ZoomConfig): void {
|
||||
this._config = config;
|
||||
public setSettings(config: PartialZoomSettings | null): void {
|
||||
this._settings = config;
|
||||
this._debouncedUpdater();
|
||||
}
|
||||
|
||||
protected _changeHandler(ev: Event): void {
|
||||
const pz = (<CustomEvent<PanzoomEventDetail>>ev).detail;
|
||||
|
||||
const isUnzoomed = this._isUnzoomed(pz.scale);
|
||||
const isAtDefault = this._isAtDefaultZoomAndPan(pz.x, pz.y, pz.scale);
|
||||
const unzoomed = this._isUnzoomed(pz.scale);
|
||||
|
||||
// Take care here to only dispatch the zoomed/unzoomed events when the
|
||||
// absolute state changes (rather than on every single zoom adjustment).
|
||||
if (isUnzoomed && this._zoomed) {
|
||||
if (unzoomed && this._zoomed) {
|
||||
this._zoomed = false;
|
||||
this._setTouchAction(true);
|
||||
dispatchFrigateCardEvent(this._element, 'zoom:unzoomed');
|
||||
} else if (!isUnzoomed && !this._zoomed) {
|
||||
} else if (!unzoomed && !this._zoomed) {
|
||||
this._zoomed = true;
|
||||
this._setTouchAction(false);
|
||||
dispatchFrigateCardEvent(this._element, 'zoom:zoomed');
|
||||
}
|
||||
|
||||
if (isAtDefault && !this._default) {
|
||||
this._default = true;
|
||||
dispatchFrigateCardEvent(this._element, 'zoom:default', { isDefault: true });
|
||||
} else if (!isAtDefault && this._default) {
|
||||
this._default = false;
|
||||
dispatchFrigateCardEvent(this._element, 'zoom:default', { isDefault: false });
|
||||
}
|
||||
const converted = this._convertXYPanToPercent(pz.x, pz.y, pz.scale);
|
||||
const observed: ZoomSettingsObserved = {
|
||||
pan: {
|
||||
x: converted?.x ?? ZOOM_DEFAULT_PAN_X,
|
||||
y: converted?.y ?? ZOOM_DEFAULT_PAN_Y,
|
||||
},
|
||||
zoom: pz.scale,
|
||||
isDefault: this._isAtDefaultZoomAndPan(pz.x, pz.y, pz.scale),
|
||||
unzoomed: unzoomed,
|
||||
};
|
||||
|
||||
dispatchFrigateCardEvent(this._element, 'zoom:change', observed);
|
||||
}
|
||||
|
||||
protected _isZoomEqual(a: ZoomConfig, b: ZoomConfig): boolean {
|
||||
protected _isZoomEqual(a: PartialZoomSettings, b: PartialZoomSettings): boolean {
|
||||
// The ?? clauses below cannot be reached since this function is only ever
|
||||
// used fully specified by this object. It's kept as-is for completeness.
|
||||
return (
|
||||
@@ -247,16 +260,8 @@ export class ZoomController {
|
||||
);
|
||||
}
|
||||
|
||||
protected _isZoomEmpty(config?: ZoomConfig | null): boolean {
|
||||
return (
|
||||
config?.pan?.x === undefined &&
|
||||
config?.pan?.y === undefined &&
|
||||
config?.zoom === undefined
|
||||
);
|
||||
}
|
||||
|
||||
protected _getConfigToUse(): ZoomConfig | null {
|
||||
return this._isZoomEmpty(this._config) ? this._defaultConfig : this._config;
|
||||
protected _getConfigToUse(): PartialZoomSettings | null {
|
||||
return isZoomEmpty(this._settings) ? this._defaultSettings : this._settings;
|
||||
}
|
||||
|
||||
protected _updateBasedOnConfig(): void {
|
||||
@@ -292,6 +297,9 @@ export class ZoomController {
|
||||
}
|
||||
|
||||
this._panzoom.zoom(desiredScale, {
|
||||
// Zoom is stepped, not animated. If it is animated, there is interaction
|
||||
// between the zoom and the pan below, and the pan would need to be
|
||||
// delayed until after the zoom is complete.
|
||||
animate: false,
|
||||
});
|
||||
|
||||
@@ -300,10 +308,12 @@ export class ZoomController {
|
||||
// situation where we need to ensure the zoom completes first. Using
|
||||
// `requestAnimationFrame` appears to reliably allow the zoom to finish
|
||||
// rendering first, before the pain is applied.
|
||||
//
|
||||
// See: https://github.com/timmywil/panzoom?tab=readme-ov-file#a-note-on-the-async-nature-of-panzoom
|
||||
window.requestAnimationFrame(() => {
|
||||
this._panzoom?.pan(x, y, {
|
||||
animate: false,
|
||||
animate: true,
|
||||
duration: 100,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -332,6 +342,28 @@ export class ZoomController {
|
||||
};
|
||||
}
|
||||
|
||||
protected _convertXYPanToPercent(
|
||||
x: number,
|
||||
y: number,
|
||||
scale: number,
|
||||
): { x: number; y: number } | null {
|
||||
const minMax = this._getTransformMinMax(scale, this._panzoom?.getScale());
|
||||
if (minMax === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
x:
|
||||
((-x + Math.abs(minMax.minX)) /
|
||||
(Math.abs(minMax.maxX) + Math.abs(minMax.minX))) *
|
||||
100,
|
||||
y:
|
||||
((-y + Math.abs(minMax.minY)) /
|
||||
(Math.abs(minMax.maxY) + Math.abs(minMax.minY))) *
|
||||
100,
|
||||
};
|
||||
}
|
||||
|
||||
protected _getTransformMinMax(
|
||||
desiredScale: number,
|
||||
currentScale?: number,
|
||||
@@ -375,14 +407,14 @@ export class ZoomController {
|
||||
}
|
||||
|
||||
protected _isAtDefaultZoomAndPan(x: number, y: number, scale: number): boolean {
|
||||
if (!this._defaultConfig) {
|
||||
if (!this._defaultSettings) {
|
||||
return this._isUnzoomed(scale);
|
||||
}
|
||||
|
||||
const convertedDefault = this._convertPercentToXYPan(
|
||||
this._defaultConfig.pan?.x ?? ZOOM_DEFAULT_PAN_X,
|
||||
this._defaultConfig.pan?.y ?? ZOOM_DEFAULT_PAN_Y,
|
||||
this._defaultConfig.zoom ?? ZOOM_DEFAULT_SCALE,
|
||||
this._defaultSettings.pan?.x ?? ZOOM_DEFAULT_PAN_X,
|
||||
this._defaultSettings.pan?.y ?? ZOOM_DEFAULT_PAN_Y,
|
||||
this._defaultSettings.zoom ?? ZOOM_DEFAULT_SCALE,
|
||||
);
|
||||
if (!convertedDefault) {
|
||||
return true;
|
||||
@@ -393,7 +425,7 @@ export class ZoomController {
|
||||
arefloatsApproximatelyEqual(y, convertedDefault.y) &&
|
||||
arefloatsApproximatelyEqual(
|
||||
scale,
|
||||
this._defaultConfig.zoom ??
|
||||
this._defaultSettings.zoom ??
|
||||
// The ZOOM_DEFAULT_SCALE clause below cannot be reached since when
|
||||
// this._defaultConfig.zoom is undefined, convertedDefault will end up
|
||||
// null above and this function will have already returned.
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { ZoomConfig, ZoomDefault } from './types.js';
|
||||
import { dispatchViewContextChangeEvent } from '../../view/view.js';
|
||||
import { ZoomSettingsObserved, PartialZoomSettings } from './types.js';
|
||||
|
||||
interface ZoomViewContext {
|
||||
observed?: ZoomSettingsObserved;
|
||||
|
||||
// Populate this to request zoom to a particular scale/x/y. An empty object
|
||||
// will reset to default, null will make no change.
|
||||
zoom?: ZoomConfig | null;
|
||||
|
||||
// This will be populated with whether or not the current zoom is at the
|
||||
// default level.
|
||||
isDefault?: boolean;
|
||||
requested?: PartialZoomSettings | null;
|
||||
}
|
||||
|
||||
interface ZoomsViewContext {
|
||||
@@ -22,36 +20,37 @@ declare module 'view' {
|
||||
}
|
||||
}
|
||||
|
||||
export const generateViewContextForZoomChange = (
|
||||
export const generateViewContextForZoom = (
|
||||
targetID: string,
|
||||
options?: {
|
||||
zoom?: ZoomConfig | null;
|
||||
isDefault?: boolean;
|
||||
observed?: ZoomSettingsObserved;
|
||||
requested?: PartialZoomSettings | null;
|
||||
},
|
||||
): ViewContext | null => {
|
||||
return {
|
||||
zoom: {
|
||||
[targetID]: {
|
||||
zoom: options?.zoom ?? null,
|
||||
...(options?.isDefault !== undefined && { isDefault: options.isDefault }),
|
||||
observed: options?.observed ?? undefined,
|
||||
requested: options?.requested ?? null,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience wrapper to convert a zoom default into a dispatched view context
|
||||
* Convenience wrapper to convert zoom settings into a dispatched view context
|
||||
* change.
|
||||
*/
|
||||
export const handleZoomDefaultEvent = (
|
||||
export const handleZoomSettingsObservedEvent = (
|
||||
element: EventTarget,
|
||||
ev: CustomEvent<ZoomDefault>,
|
||||
ev: CustomEvent<ZoomSettingsObserved>,
|
||||
targetID?: string,
|
||||
): void => {
|
||||
targetID && dispatchViewContextChangeEvent(
|
||||
element,
|
||||
generateViewContextForZoomChange(targetID, {
|
||||
isDefault: ev.detail.isDefault,
|
||||
}),
|
||||
);
|
||||
targetID &&
|
||||
dispatchViewContextChangeEvent(
|
||||
element,
|
||||
generateViewContextForZoom(targetID, {
|
||||
observed: ev.detail,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user