feat: Implement support for gesture-based PTZ control (#2378)
- Closes: #1839
This commit is contained in:
@@ -5,7 +5,6 @@ import { EqualityMap } from '../cache/equality-map.js';
|
||||
import { CardCameraAPI } from '../card-controller/types.js';
|
||||
import { sortItems } from '../card-controller/view/sort.js';
|
||||
import {
|
||||
PTZ_PAN_TILT_ACTIONS,
|
||||
PTZAction,
|
||||
PTZActionPhase,
|
||||
PTZPanTiltAction,
|
||||
@@ -910,21 +909,22 @@ export class CameraManager {
|
||||
* For example: with 90° rotation, pressing "left" should send "down" to camera.
|
||||
*/
|
||||
private _rotatePTZAction(action: PTZAction, rotation?: Rotation): PTZAction {
|
||||
if (!rotation) {
|
||||
if (
|
||||
!rotation ||
|
||||
action === 'preset' ||
|
||||
action === 'zoom_in' ||
|
||||
action === 'zoom_out'
|
||||
) {
|
||||
return action;
|
||||
}
|
||||
|
||||
// Pan/tilt directions in clockwise order for rotation calculation
|
||||
const index = PTZ_PAN_TILT_ACTIONS.indexOf(action as PTZPanTiltAction);
|
||||
|
||||
if (index === -1) {
|
||||
// Not a directional action (e.g., zoom_in, zoom_out, preset)
|
||||
return action;
|
||||
}
|
||||
// Directions in clockwise order for rotation calculation.
|
||||
const CLOCKWISE: PTZPanTiltAction[] = ['up', 'right', 'down', 'left'];
|
||||
const index = CLOCKWISE.indexOf(action);
|
||||
|
||||
// Each 90° rotation shifts the direction index counter-clockwise.
|
||||
const shift = (4 - rotation / 90) % 4;
|
||||
return PTZ_PAN_TILT_ACTIONS[(index + shift) % 4];
|
||||
return CLOCKWISE[(index + shift) % 4];
|
||||
}
|
||||
|
||||
public async executePTZAction(
|
||||
|
||||
@@ -6,8 +6,25 @@ export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActio
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const currentEnabled = api.getViewManager().getView()?.context?.ptzControls?.enabled;
|
||||
|
||||
// If `enabled` is explicit, use it. If only `type` is being changed, leave
|
||||
// `enabled` untouched (undefined = no change). Otherwise (neither set),
|
||||
// toggle the current enabled value — this is the menu-button show/hide use
|
||||
// case.
|
||||
const enabled =
|
||||
this._action.enabled ??
|
||||
(this._action.type
|
||||
? undefined
|
||||
: currentEnabled === undefined
|
||||
? undefined
|
||||
: !currentEnabled);
|
||||
|
||||
api.getViewManager().setViewWithMergedContext({
|
||||
ptzControls: { enabled: this._action.enabled },
|
||||
ptzControls: {
|
||||
...(enabled !== undefined && { enabled }),
|
||||
...(this._action.type && { type: this._action.type }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,7 +717,7 @@ export class MenuButtonController {
|
||||
style: isOn ? this._getEmphasizedStyle() : {},
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.ptz_controls'),
|
||||
tap_action: createPTZControlsAction(!isOn),
|
||||
tap_action: createPTZControlsAction({ enabled: !isOn }),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import type { Handler } from '@use-gesture/vanilla';
|
||||
import {
|
||||
createGesture,
|
||||
dragAction,
|
||||
pinchAction,
|
||||
wheelAction,
|
||||
} from '@use-gesture/vanilla';
|
||||
import { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
import { dispatchActionExecutionRequest } from '../../card-controller/actions/utils/execution-request.js';
|
||||
import {
|
||||
PTZAction,
|
||||
PTZActionPhase,
|
||||
PTZPanAction,
|
||||
PTZTiltAction,
|
||||
PTZZoomAction,
|
||||
} from '../../config/schema/actions/custom/ptz.js';
|
||||
import { createPTZAction } from '../../utils/action.js';
|
||||
|
||||
// Minimum drag distance (px) before the gesture is recognized as a drag.
|
||||
const DRAG_THRESHOLD = 50;
|
||||
|
||||
// Drag distance (px) at which continuous PTZ start/stop begins. Below this
|
||||
// threshold a single relative "nudge" is dispatched on release instead.
|
||||
const CONTINUOUS_THRESHOLD = 100;
|
||||
|
||||
const CURSOR_GRAB = 'grab';
|
||||
const CURSOR_GRABBING = 'grabbing';
|
||||
|
||||
export class PTZDragController implements ReactiveController {
|
||||
private _host: ReactiveControllerHost & HTMLElement;
|
||||
|
||||
// The DOM element gestures bind to. Distinct from _host because the host's
|
||||
// shadow DOM may contain siblings (e.g. PTZ overlay) that should not
|
||||
// receive gesture events.
|
||||
private _gestureElement: HTMLElement | null = null;
|
||||
|
||||
// The @use-gesture Recognizer instance.
|
||||
private _gesture: ReturnType<ReturnType<typeof createGesture>> | null = null;
|
||||
|
||||
// Whether gesture handling is active (triggers host re-render on change).
|
||||
private _active = false;
|
||||
|
||||
// Currently running continuous pan/tilt directions.
|
||||
private _activeX: PTZPanAction | null = null;
|
||||
private _activeY: PTZTiltAction | null = null;
|
||||
|
||||
// Whether the current drag has crossed CONTINUOUS_THRESHOLD and entered
|
||||
// continuous mode. Below that threshold, drag-end dispatches relative.
|
||||
private _continuous = false;
|
||||
|
||||
// When a pinch occurs mid-drag, the drag is "poisoned" — all remaining
|
||||
// drag events for that gesture are ignored to prevent stray pan/tilt.
|
||||
private _dragCancelledByPinch = false;
|
||||
|
||||
// Currently running continuous zoom (pinch).
|
||||
private _activeZoom: PTZZoomAction | null = null;
|
||||
|
||||
// Original element styles restored on deactivate.
|
||||
private _savedTouchAction = '';
|
||||
private _savedCursor = '';
|
||||
|
||||
constructor(host: ReactiveControllerHost & HTMLElement) {
|
||||
this._host = host;
|
||||
this._host.addController(this);
|
||||
}
|
||||
|
||||
public hostDisconnected(): void {
|
||||
this.deactivateIfNecessary();
|
||||
}
|
||||
|
||||
public activateIfNecessary(element: HTMLElement): void {
|
||||
if (this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._gestureElement = element;
|
||||
this._applyGestureStyles(element);
|
||||
|
||||
const gesture = createGesture([dragAction, pinchAction, wheelAction]);
|
||||
|
||||
this._gesture = gesture(
|
||||
element,
|
||||
{
|
||||
onDrag: this._onDrag,
|
||||
onPinch: this._onPinch,
|
||||
onWheel: this._onWheel,
|
||||
onPointerDown: () => this._setCursor(true),
|
||||
onPointerUp: () => this._setCursor(false),
|
||||
onPointerLeave: () => this._setCursor(false),
|
||||
onPointerCancel: () => this._setCursor(false),
|
||||
},
|
||||
{
|
||||
drag: {
|
||||
threshold: DRAG_THRESHOLD,
|
||||
filterTaps: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this._active = true;
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
public deactivateIfNecessary(): void {
|
||||
if (!this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._gesture?.destroy();
|
||||
this._gesture = null;
|
||||
|
||||
this._restoreGestureStyles();
|
||||
this._gestureElement = null;
|
||||
|
||||
this._stopAllDirections();
|
||||
this._stopZoom();
|
||||
this._continuous = false;
|
||||
this._dragCancelledByPinch = false;
|
||||
|
||||
this._active = false;
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
private _applyGestureStyles(element: HTMLElement): void {
|
||||
this._savedCursor = element.style.cursor;
|
||||
this._savedTouchAction = element.style.touchAction;
|
||||
element.style.cursor = CURSOR_GRAB;
|
||||
element.style.touchAction = 'none';
|
||||
}
|
||||
|
||||
private _restoreGestureStyles(): void {
|
||||
/* istanbul ignore next: only called when gesture is active -- @preserve */
|
||||
if (this._gestureElement) {
|
||||
this._gestureElement.style.cursor = this._savedCursor;
|
||||
this._gestureElement.style.touchAction = this._savedTouchAction;
|
||||
}
|
||||
}
|
||||
|
||||
private _setCursor(grabbing: boolean): void {
|
||||
/* istanbul ignore next: only called when gesture is active -- @preserve */
|
||||
if (this._gestureElement) {
|
||||
this._gestureElement.style.cursor = grabbing ? CURSOR_GRABBING : CURSOR_GRAB;
|
||||
}
|
||||
}
|
||||
|
||||
private _onDrag: Handler<'drag'> = (state) => {
|
||||
if (state.pinching) {
|
||||
this._stopAllDirections();
|
||||
this._dragCancelledByPinch = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._dragCancelledByPinch) {
|
||||
if (state.last) {
|
||||
this._dragCancelledByPinch = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [mx, my] = state.movement;
|
||||
|
||||
if (state.last) {
|
||||
if (this._continuous) {
|
||||
this._stopAllDirections();
|
||||
this._continuous = false;
|
||||
} else {
|
||||
// Short drag: dispatch a single relative action per axis.
|
||||
this._dispatchRelative(mx, my);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const distance = Math.sqrt(mx * mx + my * my);
|
||||
|
||||
if (!this._continuous && distance >= CONTINUOUS_THRESHOLD) {
|
||||
this._continuous = true;
|
||||
}
|
||||
|
||||
if (!this._continuous) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inverted: dragging right sends PTZ left ("grab the scene").
|
||||
const wantX: PTZPanAction | null = mx > 0 ? 'left' : mx < 0 ? 'right' : null;
|
||||
const wantY: PTZTiltAction | null = my > 0 ? 'up' : my < 0 ? 'down' : null;
|
||||
|
||||
if (wantX !== this._activeX) {
|
||||
if (this._activeX) {
|
||||
this._dispatch(this._activeX, 'stop');
|
||||
}
|
||||
this._activeX = wantX;
|
||||
if (wantX) {
|
||||
this._dispatch(wantX, 'start');
|
||||
}
|
||||
}
|
||||
|
||||
if (wantY !== this._activeY) {
|
||||
if (this._activeY) {
|
||||
this._dispatch(this._activeY, 'stop');
|
||||
}
|
||||
this._activeY = wantY;
|
||||
if (wantY) {
|
||||
this._dispatch(wantY, 'start');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _onPinch: Handler<'pinch'> = (state) => {
|
||||
const direction = state.direction[0];
|
||||
|
||||
if (state.last) {
|
||||
this._stopZoom();
|
||||
return;
|
||||
}
|
||||
|
||||
if (direction === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action: PTZZoomAction = direction > 0 ? 'zoom_in' : 'zoom_out';
|
||||
|
||||
if (action !== this._activeZoom) {
|
||||
this._stopZoom();
|
||||
this._activeZoom = action;
|
||||
this._dispatch(action, 'start');
|
||||
}
|
||||
};
|
||||
|
||||
private _onWheel: Handler<'wheel'> = (state) => {
|
||||
// delta[1] is the vertical (Y-axis) scroll amount.
|
||||
const dy = state.delta[1];
|
||||
if (dy === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatch(dy > 0 ? 'zoom_out' : 'zoom_in');
|
||||
};
|
||||
|
||||
private _dispatch(action: PTZAction | null, phase?: PTZActionPhase): void {
|
||||
/* istanbul ignore next: all call sites guard against this -- @preserve */
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
actions: createPTZAction({
|
||||
ptzAction: action,
|
||||
...(phase && { ptzPhase: phase }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch relative actions from movement values. Inverted: positive X (drag
|
||||
// right) sends PTZ left.
|
||||
private _dispatchRelative(mx: number, my: number): void {
|
||||
if (mx > 0) {
|
||||
this._dispatch('left');
|
||||
} else if (mx < 0) {
|
||||
this._dispatch('right');
|
||||
}
|
||||
if (my > 0) {
|
||||
this._dispatch('up');
|
||||
} else if (my < 0) {
|
||||
this._dispatch('down');
|
||||
}
|
||||
}
|
||||
|
||||
private _stopAllDirections(): void {
|
||||
if (this._activeX) {
|
||||
this._dispatch(this._activeX, 'stop');
|
||||
this._activeX = null;
|
||||
}
|
||||
if (this._activeY) {
|
||||
this._dispatch(this._activeY, 'stop');
|
||||
this._activeY = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _stopZoom(): void {
|
||||
if (!this._activeZoom) {
|
||||
return;
|
||||
}
|
||||
this._dispatch(this._activeZoom, 'stop');
|
||||
this._activeZoom = null;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,17 @@ import { dispatchActionExecutionRequest } from '../../card-controller/actions/ut
|
||||
import { SubmenuInteraction } from '../../components/submenu/types.js';
|
||||
import { PTZAction } from '../../config/schema/actions/custom/ptz.js';
|
||||
import { Actions, ActionsConfig } from '../../config/schema/actions/types.js';
|
||||
import { PTZControlsConfig } from '../../config/schema/common/controls/ptz.js';
|
||||
import {
|
||||
PTZControlsConfig,
|
||||
PTZControlType,
|
||||
} from '../../config/schema/common/controls/ptz.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { Interaction } from '../../types.js';
|
||||
import { createPTZMultiAction, getActionConfigGivenAction } from '../../utils/action.js';
|
||||
import {
|
||||
createPTZControlsAction,
|
||||
createPTZMultiAction,
|
||||
getActionConfigGivenAction,
|
||||
} from '../../utils/action.js';
|
||||
import { PTZControllerActions } from './types';
|
||||
|
||||
export class PTZController {
|
||||
@@ -69,14 +76,28 @@ export class PTZController {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleTypeHandler(ev: Event, currentType?: PTZControlType): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
dispatchActionExecutionRequest(this._host, {
|
||||
actions: createPTZControlsAction({
|
||||
type: currentType === 'gestures' ? 'buttons' : 'gestures',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
public hasPhysicalPTZ(): boolean {
|
||||
return (
|
||||
!!this._cameraID &&
|
||||
!!this._cameraManager?.getCameraCapabilities(this._cameraID)?.hasPTZCapability()
|
||||
);
|
||||
}
|
||||
|
||||
public shouldDisplay(): boolean {
|
||||
return this._forceVisibility !== undefined
|
||||
? this._forceVisibility
|
||||
: this._config?.mode === 'auto'
|
||||
? !!this._cameraID &&
|
||||
!!this._cameraManager
|
||||
?.getCameraCapabilities(this._cameraID)
|
||||
?.hasPTZCapability()
|
||||
? this.hasPhysicalPTZ()
|
||||
: this._config?.mode === 'on';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PTZControlAction } from '../../config/schema/actions/custom/ptz';
|
||||
import { Actions } from '../../config/schema/actions/types';
|
||||
import { PTZControlType } from '../../config/schema/common/controls/ptz';
|
||||
|
||||
interface PTZControlsViewContext {
|
||||
enabled?: boolean;
|
||||
type?: PTZControlType;
|
||||
}
|
||||
declare module 'view' {
|
||||
interface ViewContext {
|
||||
|
||||
@@ -19,12 +19,13 @@ export class ZoomController {
|
||||
// Is the controller zoomed in at all?
|
||||
private _zoomed = false;
|
||||
|
||||
// Is the controller set to the default zoom/pan settings?
|
||||
private _default = true;
|
||||
|
||||
// Should clicks be allowed to propagate, or consumed as a pan/zoom action?
|
||||
private _allowClick = true;
|
||||
|
||||
// Whether zoom/pan gestures are active. When `false`, all gesture events pass
|
||||
// through untouched (used to yield to PTZ gesture mode).
|
||||
private _zoom = true;
|
||||
|
||||
private _defaultSettings: PartialZoomSettings | null;
|
||||
private _settings: PartialZoomSettings | null;
|
||||
|
||||
@@ -198,6 +199,14 @@ export class ZoomController {
|
||||
this._debouncedUpdater();
|
||||
}
|
||||
|
||||
public isActivated(): boolean {
|
||||
return !!this._panzoom;
|
||||
}
|
||||
|
||||
public setZoom(value: boolean): void {
|
||||
this._zoom = value;
|
||||
}
|
||||
|
||||
private _changeHandler(ev: Event): void {
|
||||
const pz = (<CustomEvent<PanzoomEventDetail>>ev).detail;
|
||||
const unzoomed = this._isUnzoomed(pz.scale);
|
||||
@@ -439,6 +448,9 @@ export class ZoomController {
|
||||
}
|
||||
|
||||
private _shouldZoomOrPan(ev: Event): boolean {
|
||||
if (!this._zoom) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!this._isUnzoomed(this._panzoom?.getScale()) ||
|
||||
// TouchEvent does not exist on Firefox on non-touch events. See:
|
||||
|
||||
@@ -15,8 +15,13 @@ import { MicrophoneState } from '../../card-controller/types.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
||||
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
|
||||
import { PTZDragController } from '../../components-lib/ptz/drag-controller.js';
|
||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import {
|
||||
ptzControlsDefaults,
|
||||
PTZControlType,
|
||||
} from '../../config/schema/common/controls/ptz.js';
|
||||
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
||||
import { LiveConfig } from '../../config/schema/live.js';
|
||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||
@@ -32,7 +37,6 @@ import '../carousel';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { AdvancedCameraCardPTZ } from '../ptz.js';
|
||||
import './provider.js';
|
||||
|
||||
const ADVANCED_CAMERA_CARD_LIVE_PROVIDER = 'advanced-camera-card-live-provider';
|
||||
@@ -70,14 +74,13 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public viewFilterCameraID?: string;
|
||||
|
||||
// Index between camera name and slide number.
|
||||
private _cameraToSlide: Record<string, number> = {};
|
||||
private _refPTZControl: Ref<AdvancedCameraCardPTZ> = createRef();
|
||||
private _refCarousel: Ref<HTMLElement> = createRef();
|
||||
|
||||
private _mediaActionsController = new MediaActionsController();
|
||||
private _mediaHeightController = new MediaHeightController(this, '.embla__slide');
|
||||
|
||||
private _ptzDragController = new PTZDragController(this);
|
||||
|
||||
@state()
|
||||
private _mediaHasLoaded = false;
|
||||
|
||||
@@ -96,10 +99,38 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _getTransitionEffect(): TransitionEffect {
|
||||
return this.liveConfig?.transition_effect ?? configDefaults.live.transition_effect;
|
||||
private _getDisplayPTZType(cameraID: string | null): PTZControlType {
|
||||
if (
|
||||
!cameraID ||
|
||||
// For cameras without physical PTZ, always display buttons so the user
|
||||
// can control digital zoom. Gesture type controls have no effect on those
|
||||
// cameras.
|
||||
!this.cameraManager?.getCameraCapabilities(cameraID)?.hasPTZCapability()
|
||||
) {
|
||||
return 'buttons';
|
||||
}
|
||||
return (
|
||||
this.viewManagerEpoch?.manager.getView()?.context?.ptzControls?.type ??
|
||||
this.liveConfig?.controls.ptz.type ??
|
||||
ptzControlsDefaults.type
|
||||
);
|
||||
}
|
||||
|
||||
private _isGesturesPTZActive(
|
||||
view: View | null | undefined,
|
||||
cameraID: string | null,
|
||||
): boolean {
|
||||
// _getDisplayPTZType returns 'buttons' for digital-only cameras, so this
|
||||
// implicitly guards against cameras without physical PTZ capability.
|
||||
return (
|
||||
this._getDisplayPTZType(cameraID) === 'gestures' &&
|
||||
view?.context?.ptzControls?.enabled !== false
|
||||
);
|
||||
}
|
||||
|
||||
private _getTransitionEffect = (): TransitionEffect =>
|
||||
this.liveConfig?.transition_effect ?? configDefaults.live.transition_effect;
|
||||
|
||||
private _getSelectedCameraIndex(): number {
|
||||
if (this.viewFilterCameraID) {
|
||||
// If the carousel is limited to a single cameraID, the first (only)
|
||||
@@ -144,21 +175,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
return [AutoMediaLoadedInfo()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of slides to lazily load. 0 means all slides are lazy
|
||||
* loaded, 1 means that 1 slide on each side of the currently selected slide
|
||||
* should lazy load, etc. `null` means lazy loading is disabled and everything
|
||||
* should load simultaneously.
|
||||
* @returns
|
||||
*/
|
||||
private _getLazyLoadCount(): number | null {
|
||||
// Defaults to fully-lazy loading.
|
||||
return this.liveConfig?.lazy_load === false ? null : 0;
|
||||
}
|
||||
|
||||
private _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||
private _getSlides(): TemplateResult[] {
|
||||
if (!this.cameraManager) {
|
||||
return [[], {}];
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
@@ -167,16 +186,13 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
: this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
|
||||
for (const cameraID of cameraIDs ?? []) {
|
||||
const slide = this._renderLive(this._getSubstreamCameraID(cameraID, view));
|
||||
if (slide) {
|
||||
cameraToSlide[cameraID] = slides.length;
|
||||
slides.push(slide);
|
||||
}
|
||||
}
|
||||
return [slides, cameraToSlide];
|
||||
return slides;
|
||||
}
|
||||
|
||||
private _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
||||
@@ -221,6 +237,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
||||
.zoom=${!this._isGesturesPTZActive(view, cameraID)}
|
||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||
handleZoomSettingsObservedEvent(
|
||||
ev,
|
||||
@@ -312,8 +329,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const [slides, cameraToSlide] = this._getSlides();
|
||||
this._cameraToSlide = cameraToSlide;
|
||||
const slides = this._getSlides();
|
||||
if (!slides.length) {
|
||||
return;
|
||||
}
|
||||
@@ -321,6 +337,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
const hasMultipleCameras = slides.length > 1;
|
||||
const neighbors = this._getCameraNeighbors();
|
||||
|
||||
const streamAwareCameraID = getStreamCameraID(view, this.viewFilterCameraID);
|
||||
const gesturesPTZActive = this._isGesturesPTZActive(view, streamAwareCameraID);
|
||||
|
||||
const forcePTZVisibility =
|
||||
!this._mediaHasLoaded ||
|
||||
(!!this.viewFilterCameraID && this.viewFilterCameraID !== view.camera) ||
|
||||
@@ -328,6 +347,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
? false
|
||||
: view.context?.ptzControls?.enabled;
|
||||
|
||||
const dragEnabled =
|
||||
hasMultipleCameras && this.liveConfig?.draggable && !gesturesPTZActive;
|
||||
|
||||
// Notes on the below:
|
||||
// - guard() is used to avoid reseting the carousel unless the
|
||||
// options/plugins actually change.
|
||||
@@ -336,7 +358,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
<advanced-camera-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
.loop=${hasMultipleCameras}
|
||||
.dragEnabled=${hasMultipleCameras && this.liveConfig?.draggable}
|
||||
.dragEnabled=${dragEnabled}
|
||||
.plugins=${guard(
|
||||
[this.cameraManager, this.liveConfig],
|
||||
this._getPlugins.bind(this),
|
||||
@@ -363,8 +385,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.config=${this.liveConfig.controls.ptz}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cameraID=${getStreamCameraID(view, this.viewFilterCameraID)}
|
||||
.cameraID=${streamAwareCameraID}
|
||||
.forceVisibility=${forcePTZVisibility}
|
||||
.type=${this._getDisplayPTZType(streamAwareCameraID)}
|
||||
>
|
||||
</advanced-camera-card-ptz>
|
||||
`;
|
||||
@@ -402,6 +425,18 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
if (rootChanged || changedProperties.has('viewManagerEpoch')) {
|
||||
this._setMediaTarget();
|
||||
}
|
||||
|
||||
const carouselEl = this._refCarousel.value;
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const streamAwareCameraID = view
|
||||
? getStreamCameraID(view, this.viewFilterCameraID)
|
||||
: null;
|
||||
|
||||
if (this._isGesturesPTZActive(view, streamAwareCameraID) && carouselEl) {
|
||||
this._ptzDragController.activateIfNecessary(carouselEl);
|
||||
} else {
|
||||
this._ptzDragController.deactivateIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -61,9 +61,15 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
@property({ attribute: false })
|
||||
public zoomSettings?: PartialZoomSettings | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public zoom = true;
|
||||
|
||||
@state()
|
||||
private _isVideoMediaLoaded = false;
|
||||
|
||||
@state()
|
||||
private _zoomed = false;
|
||||
|
||||
@state()
|
||||
private _hasProviderError = false;
|
||||
|
||||
@@ -177,6 +183,16 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
return result;
|
||||
}
|
||||
|
||||
// Builtin (native) video controls require all three conditions:
|
||||
// - controls.builtin: user config enables native controls.
|
||||
// - zoom: Whether digital zoom/panning is allowed (this will be false when a
|
||||
// 'gesture' type PTZ control is active).
|
||||
// - !_zoomed: the user has not actually digital zoomed in (when zoomed, we
|
||||
// want to hide the controls).
|
||||
private _getEffectiveBuiltinControls(): boolean {
|
||||
return !!this.liveConfig?.controls.builtin && this.zoom && !this._zoomed;
|
||||
}
|
||||
|
||||
private _renderContainer(template: TemplateResult): TemplateResult {
|
||||
const config = this.camera?.getConfig();
|
||||
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||
@@ -203,10 +219,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
: undefined,
|
||||
)}
|
||||
.settings=${this.zoomSettings}
|
||||
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls(false)}
|
||||
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls()}
|
||||
.zoom=${this.zoom}
|
||||
@advanced-camera-card:zoom:zoomed=${() => (this._zoomed = true)}
|
||||
@advanced-camera-card:zoom:unzoomed=${() => (this._zoomed = false)}
|
||||
>
|
||||
${intermediateTemplate}
|
||||
</advanced-camera-card-zoomer>`
|
||||
@@ -303,7 +318,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
class=${classMap(classes)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${cameraConfig}
|
||||
?controls=${this.liveConfig.controls.builtin}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
>
|
||||
</advanced-camera-card-live-ha>`
|
||||
@@ -316,7 +331,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.microphoneState=${this.microphoneState}
|
||||
.microphoneConfig=${this.liveConfig.microphone}
|
||||
?controls=${this.liveConfig.controls.builtin}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
>
|
||||
</advanced-camera-card-live-go2rtc>`
|
||||
@@ -328,7 +343,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
?controls=${this.liveConfig.controls.builtin}
|
||||
?controls=${this._getEffectiveBuiltinControls()}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
>
|
||||
</advanced-camera-card-live-webrtc-card>`
|
||||
|
||||
+37
-5
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
@@ -13,7 +13,10 @@ import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { PTZController } from '../components-lib/ptz/ptz-controller.js';
|
||||
import { PTZControllerActions } from '../components-lib/ptz/types.js';
|
||||
import { Actions } from '../config/schema/actions/types.js';
|
||||
import { PTZControlsConfig } from '../config/schema/common/controls/ptz.js';
|
||||
import {
|
||||
PTZControlsConfig,
|
||||
PTZControlType,
|
||||
} from '../config/schema/common/controls/ptz.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import ptzStyle from '../scss/ptz.scss';
|
||||
@@ -40,6 +43,9 @@ export class AdvancedCameraCardPTZ extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public forceVisibility?: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
public type?: PTZControlType;
|
||||
|
||||
private _controller = new PTZController(this);
|
||||
private _actions: PTZControllerActions | null = null;
|
||||
|
||||
@@ -106,8 +112,10 @@ export class AdvancedCameraCardPTZ extends LitElement {
|
||||
: null;
|
||||
|
||||
const config = this._controller.getConfig();
|
||||
const isGestures = this.type === 'gestures';
|
||||
return html` <div class="ptz">
|
||||
${!config?.hide_pan_tilt &&
|
||||
${!isGestures &&
|
||||
!config?.hide_pan_tilt &&
|
||||
(this._actions?.left ||
|
||||
this._actions?.right ||
|
||||
this._actions?.up ||
|
||||
@@ -119,13 +127,17 @@ export class AdvancedCameraCardPTZ extends LitElement {
|
||||
${renderIcon('down', 'mdi:arrow-down', { actions: this._actions?.down })}
|
||||
</div>`
|
||||
: ''}
|
||||
${!config?.hide_zoom && (this._actions?.zoom_in || this._actions?.zoom_out)
|
||||
${!isGestures &&
|
||||
!config?.hide_zoom &&
|
||||
(this._actions?.zoom_in || this._actions?.zoom_out)
|
||||
? html` <div class="ptz-zoom">
|
||||
${renderIcon('zoom_in', 'mdi:plus', { actions: this._actions.zoom_in })}
|
||||
${renderIcon('zoom_out', 'mdi:minus', { actions: this._actions.zoom_out })}
|
||||
</div>`
|
||||
: html``}
|
||||
${!config?.hide_home && (this._actions?.home || presetSubmenuItems?.length)
|
||||
${!isGestures &&
|
||||
!config?.hide_home &&
|
||||
(this._actions?.home || presetSubmenuItems?.length)
|
||||
? html`<div class="ptz-presets">
|
||||
${renderIcon('home', 'mdi:home', { actions: this._actions?.home })}
|
||||
${presetSubmenuItems?.length
|
||||
@@ -149,6 +161,26 @@ export class AdvancedCameraCardPTZ extends LitElement {
|
||||
: ''}
|
||||
</div>`
|
||||
: ''}
|
||||
${
|
||||
// The type toggle switches between buttons and gestures. On
|
||||
// digital-only cameras, gestures have no effect (the drag controller
|
||||
// never activates), so the toggle is meaningless and hidden in that
|
||||
// case.
|
||||
!config?.hide_type && this._controller.hasPhysicalPTZ()
|
||||
? html`<div
|
||||
class="ptz-type"
|
||||
@click=${(ev: Event) => this._controller.toggleTypeHandler(ev, this.type)}
|
||||
>
|
||||
<advanced-camera-card-icon
|
||||
class=${classMap({
|
||||
selected: this.type === 'gestures',
|
||||
})}
|
||||
.icon=${{ icon: 'mdi:cursor-pointer' }}
|
||||
.title=${localize('elements.ptz.type')}
|
||||
></advanced-camera-card-icon>
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
+19
-20
@@ -13,17 +13,20 @@ import { PartialZoomSettings } from '../components-lib/zoom/types.js';
|
||||
|
||||
@customElement('advanced-camera-card-zoomer')
|
||||
export class AdvancedCameraCardZoomer extends LitElement {
|
||||
private _zoom: ZoomController | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public defaultSettings?: PartialZoomSettings;
|
||||
|
||||
@property({ attribute: false })
|
||||
public settings?: PartialZoomSettings | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public zoom = true;
|
||||
|
||||
@state()
|
||||
private _zoomed = false;
|
||||
|
||||
private _zoomController = new ZoomController(this);
|
||||
|
||||
private _zoomHandler = () => (this._zoomed = true);
|
||||
private _unzoomHandler = () => (this._zoomed = false);
|
||||
|
||||
@@ -37,7 +40,7 @@ export class AdvancedCameraCardZoomer extends LitElement {
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._zoom?.deactivate();
|
||||
this._zoomController.deactivate();
|
||||
this.removeEventListener('advanced-camera-card:zoom:zoomed', this._zoomHandler);
|
||||
this.removeEventListener('advanced-camera-card:zoom:unzoomed', this._unzoomHandler);
|
||||
super.disconnectedCallback();
|
||||
@@ -48,22 +51,19 @@ export class AdvancedCameraCardZoomer extends LitElement {
|
||||
setOrRemoveAttribute(this, this._zoomed, 'zoomed');
|
||||
}
|
||||
|
||||
if (this._zoom) {
|
||||
if (changedProps.has('defaultSettings')) {
|
||||
this._zoom.setDefaultSettings(this.defaultSettings ?? null);
|
||||
}
|
||||
// If config is null, make no change to the zoom.
|
||||
if (changedProps.has('settings') && this.settings) {
|
||||
this._zoom.setSettings(this.settings);
|
||||
}
|
||||
} else {
|
||||
// Ensure that the configuration will be set before activation (vs
|
||||
// activating in `connectedCallback`).
|
||||
this._zoom = new ZoomController(this, {
|
||||
config: this.settings,
|
||||
defaultConfig: this.defaultSettings,
|
||||
});
|
||||
this._zoom.activate();
|
||||
if (changedProps.has('zoom')) {
|
||||
this._zoomController.setZoom(this.zoom);
|
||||
}
|
||||
if (changedProps.has('defaultSettings')) {
|
||||
this._zoomController.setDefaultSettings(this.defaultSettings ?? null);
|
||||
}
|
||||
// If config is null, make no change to the zoom.
|
||||
if (changedProps.has('settings') && this.settings) {
|
||||
this._zoomController.setSettings(this.settings);
|
||||
}
|
||||
|
||||
if (!this._zoomController.isActivated()) {
|
||||
this._zoomController.activate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ export class AdvancedCameraCardZoomer extends LitElement {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
cursor: auto;
|
||||
}
|
||||
:host([zoomed]) {
|
||||
cursor: move;
|
||||
|
||||
@@ -633,7 +633,9 @@ const ptzControlSettingsTransform = (data: unknown): unknown => {
|
||||
'hide_pan_tilt',
|
||||
'hide_zoom',
|
||||
'hide_home',
|
||||
'hide_type',
|
||||
'style',
|
||||
'type',
|
||||
];
|
||||
|
||||
const keys = Object.keys(data);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { PTZ_CONTROL_TYPES } from '../../common/controls/ptz';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './base';
|
||||
|
||||
export const ptzControlsActionConfigSchema =
|
||||
advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
advanced_camera_card_action: z.literal('ptz_controls'),
|
||||
enabled: z.boolean(),
|
||||
enabled: z.boolean().optional(),
|
||||
type: z.enum(PTZ_CONTROL_TYPES).optional(),
|
||||
});
|
||||
export type PTZControlsActionConfig = z.infer<typeof ptzControlsActionConfigSchema>;
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
import { advancedCameraCardCustomActionsBaseSchema } from './base';
|
||||
|
||||
export const PTZ_PAN_TILT_ACTIONS = ['up', 'right', 'down', 'left'] as const;
|
||||
const PTZ_PAN_ACTIONS = ['left', 'right'] as const;
|
||||
export type PTZPanAction = (typeof PTZ_PAN_ACTIONS)[number];
|
||||
|
||||
const PTZ_TILT_ACTIONS = ['up', 'down'] as const;
|
||||
export type PTZTiltAction = (typeof PTZ_TILT_ACTIONS)[number];
|
||||
|
||||
const PTZ_PAN_TILT_ACTIONS = [...PTZ_PAN_ACTIONS, ...PTZ_TILT_ACTIONS] as const;
|
||||
export type PTZPanTiltAction = (typeof PTZ_PAN_TILT_ACTIONS)[number];
|
||||
|
||||
const PTZ_ZOOM_ACTIONS = ['zoom_in', 'zoom_out'] as const;
|
||||
export type PTZZoomAction = (typeof PTZ_ZOOM_ACTIONS)[number];
|
||||
const PTZ_BASE_ACTIONS = [...PTZ_PAN_TILT_ACTIONS, ...PTZ_ZOOM_ACTIONS] as const;
|
||||
export type PTZBaseAction = (typeof PTZ_BASE_ACTIONS)[number];
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PTZ_CONTROL_TYPES = ['buttons', 'gestures'] as const;
|
||||
export type PTZControlType = (typeof PTZ_CONTROL_TYPES)[number];
|
||||
|
||||
export const ptzControlsDefaults = {
|
||||
orientation: 'horizontal' as const,
|
||||
mode: 'auto' as const,
|
||||
hide_pan_tilt: false,
|
||||
hide_zoom: false,
|
||||
hide_home: false,
|
||||
hide_type: false,
|
||||
position: 'bottom-right' as const,
|
||||
type: 'buttons' as const,
|
||||
};
|
||||
|
||||
export const ptzControlsConfigSchema = z.object({
|
||||
@@ -21,6 +26,9 @@ export const ptzControlsConfigSchema = z.object({
|
||||
hide_pan_tilt: z.boolean().default(ptzControlsDefaults.hide_pan_tilt),
|
||||
hide_zoom: z.boolean().default(ptzControlsDefaults.hide_zoom),
|
||||
hide_home: z.boolean().default(ptzControlsDefaults.hide_home),
|
||||
hide_type: z.boolean().default(ptzControlsDefaults.hide_type),
|
||||
|
||||
type: z.enum(PTZ_CONTROL_TYPES).default(ptzControlsDefaults.type),
|
||||
|
||||
style: z.looseObject({}).optional(),
|
||||
});
|
||||
|
||||
@@ -290,6 +290,8 @@ export const CONF_LIVE_CONTROLS_PTZ_HIDE_HOME =
|
||||
`${CONF_LIVE}.controls.ptz.hide_home` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT =
|
||||
`${CONF_LIVE}.controls.ptz.hide_pan_tilt` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_HIDE_TYPE =
|
||||
`${CONF_LIVE}.controls.ptz.hide_type` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM =
|
||||
`${CONF_LIVE}.controls.ptz.hide_zoom` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_MODE = `${CONF_LIVE}.controls.ptz.mode` as const;
|
||||
@@ -297,6 +299,7 @@ export const CONF_LIVE_CONTROLS_PTZ_ORIENTATION =
|
||||
`${CONF_LIVE}.controls.ptz.orientation` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_POSITION =
|
||||
`${CONF_LIVE}.controls.ptz.position` as const;
|
||||
export const CONF_LIVE_CONTROLS_PTZ_TYPE = `${CONF_LIVE}.controls.ptz.type` as const;
|
||||
export const CONF_LIVE_CONTROLS_WHEEL = `${CONF_LIVE}.controls.wheel` as const;
|
||||
|
||||
export const CONF_LIVE_CONTROLS_THUMBNAILS_MODE =
|
||||
|
||||
@@ -121,10 +121,12 @@ import {
|
||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
|
||||
CONF_LIVE_CONTROLS_PTZ_HIDE_HOME,
|
||||
CONF_LIVE_CONTROLS_PTZ_HIDE_PAN_TILT,
|
||||
CONF_LIVE_CONTROLS_PTZ_HIDE_TYPE,
|
||||
CONF_LIVE_CONTROLS_PTZ_HIDE_ZOOM,
|
||||
CONF_LIVE_CONTROLS_PTZ_MODE,
|
||||
CONF_LIVE_CONTROLS_PTZ_ORIENTATION,
|
||||
CONF_LIVE_CONTROLS_PTZ_POSITION,
|
||||
CONF_LIVE_CONTROLS_PTZ_TYPE,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
|
||||
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
|
||||
@@ -860,6 +862,18 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
},
|
||||
];
|
||||
|
||||
private _ptzTypes: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{
|
||||
value: 'buttons',
|
||||
label: localize('config.live.controls.ptz.types.buttons'),
|
||||
},
|
||||
{
|
||||
value: 'gestures',
|
||||
label: localize('config.live.controls.ptz.types.gestures'),
|
||||
},
|
||||
];
|
||||
|
||||
private _ptzPositions: EditorSelectOption[] = [
|
||||
{ value: '', label: '' },
|
||||
{
|
||||
@@ -3225,6 +3239,10 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
CONF_LIVE_CONTROLS_PTZ_MODE,
|
||||
this._ptzModes,
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
CONF_LIVE_CONTROLS_PTZ_TYPE,
|
||||
this._ptzTypes,
|
||||
)}
|
||||
${this._renderOptionSelector(
|
||||
CONF_LIVE_CONTROLS_PTZ_POSITION,
|
||||
this._ptzPositions,
|
||||
@@ -3254,6 +3272,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
||||
label: localize('config.live.controls.ptz.hide_home'),
|
||||
},
|
||||
)}
|
||||
${this._renderSwitch(
|
||||
CONF_LIVE_CONTROLS_PTZ_HIDE_TYPE,
|
||||
this._defaults.live.controls.ptz.hide_type,
|
||||
{
|
||||
label: localize('config.live.controls.ptz.hide_type'),
|
||||
},
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
`,
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
},
|
||||
"disable": "Disable",
|
||||
"disable_except": "Disable except",
|
||||
"force": "Force",
|
||||
"editor_label": "Camera capabilities"
|
||||
"editor_label": "Camera capabilities",
|
||||
"force": "Force"
|
||||
},
|
||||
"cast": {
|
||||
"dashboard": {
|
||||
@@ -388,6 +388,7 @@
|
||||
"editor_label": "PTZ",
|
||||
"hide_home": "Hide home & preset controls",
|
||||
"hide_pan_tilt": "Hide pan & tilt control",
|
||||
"hide_type": "Hide type toggle button",
|
||||
"hide_zoom": "Hide zoom control",
|
||||
"mode": "Mode",
|
||||
"modes": {
|
||||
@@ -405,6 +406,11 @@
|
||||
"bottom-right": "Bottom right",
|
||||
"top-left": "Top left",
|
||||
"top-right": "Top right"
|
||||
},
|
||||
"type": "Default type",
|
||||
"types": {
|
||||
"buttons": "Buttons",
|
||||
"gestures": "Gestures"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -668,8 +674,8 @@
|
||||
"cameras_secondary": "What cameras to render on this card",
|
||||
"delete": "Delete",
|
||||
"dimensions": "Card dimensions",
|
||||
"docs": "Documentation",
|
||||
"dimensions_secondary": "Card dimensions & shape options",
|
||||
"docs": "Documentation",
|
||||
"folders": "Folders",
|
||||
"folders_secondary": "What folders to render on this card",
|
||||
"image": "Image",
|
||||
@@ -692,11 +698,11 @@
|
||||
"profiles_secondary": "Choose pre-configured sets of defaults",
|
||||
"remote_control": "Remote Control",
|
||||
"remote_control_secondary": "Options for remote controlling the card",
|
||||
"toggle_diagnostics": "Toggle diagnostics",
|
||||
"status_bar": "Status bar",
|
||||
"status_bar_secondary": "Status bar look & feel options",
|
||||
"timeline": "Timeline",
|
||||
"timeline_secondary": "Event timeline options",
|
||||
"toggle_diagnostics": "Toggle diagnostics",
|
||||
"upgrade": "Automatic Upgrade",
|
||||
"upgrade_available": "An automatic card configuration upgrade is available",
|
||||
"view": "View",
|
||||
@@ -709,6 +715,7 @@
|
||||
"left": "Left",
|
||||
"presets": "Presets",
|
||||
"right": "Right",
|
||||
"type": "PTZ Type",
|
||||
"up": "Up",
|
||||
"zoom_in": "Zoom In",
|
||||
"zoom_out": "Zoom Out"
|
||||
|
||||
+22
-7
@@ -46,7 +46,8 @@
|
||||
|
||||
.ptz-move,
|
||||
.ptz-zoom,
|
||||
.ptz-presets {
|
||||
.ptz-presets,
|
||||
.ptz-type {
|
||||
position: relative;
|
||||
|
||||
transition:
|
||||
@@ -70,16 +71,19 @@
|
||||
}
|
||||
|
||||
:host([data-orientation='horizontal']) .ptz .ptz-zoom,
|
||||
:host([data-orientation='horizontal']) .ptz .ptz-presets {
|
||||
:host([data-orientation='horizontal']) .ptz .ptz-presets,
|
||||
:host([data-orientation='horizontal']) .ptz .ptz-type {
|
||||
width: calc(var(--advanced-camera-card-ptz-icon-size) * 1.5);
|
||||
}
|
||||
:host([data-orientation='vertical']) .ptz .ptz-zoom,
|
||||
:host([data-orientation='vertical']) .ptz .ptz-presets {
|
||||
:host([data-orientation='vertical']) .ptz .ptz-presets,
|
||||
:host([data-orientation='vertical']) .ptz .ptz-type {
|
||||
height: calc(var(--advanced-camera-card-ptz-icon-size) * 1.5);
|
||||
}
|
||||
|
||||
.ptz-zoom,
|
||||
.ptz-presets {
|
||||
.ptz-presets,
|
||||
.ptz-type {
|
||||
border-radius: var(--advanced-camera-card-border-radius-final);
|
||||
}
|
||||
|
||||
@@ -119,16 +123,27 @@ advanced-camera-card-submenu:not(.disabled) {
|
||||
}
|
||||
|
||||
.ptz-presets,
|
||||
.ptz-zoom {
|
||||
.ptz-zoom,
|
||||
.ptz-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
:host([data-orientation='vertical']) .ptz-presets,
|
||||
:host([data-orientation='vertical']) .ptz-zoom {
|
||||
:host([data-orientation='vertical']) .ptz-zoom,
|
||||
:host([data-orientation='vertical']) .ptz-type {
|
||||
flex-direction: row;
|
||||
}
|
||||
:host([data-orientation='horizontal']) .ptz-presets,
|
||||
:host([data-orientation='horizontal']) .ptz-zoom {
|
||||
:host([data-orientation='horizontal']) .ptz-zoom,
|
||||
:host([data-orientation='horizontal']) .ptz-type {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ptz-type {
|
||||
cursor: pointer;
|
||||
|
||||
advanced-camera-card-icon.selected {
|
||||
color: var(--advanced-camera-card-ptz-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@
|
||||
var(--advanced-camera-card-ptz-background),
|
||||
transparent var(--advanced-camera-card-control-background-opacity)
|
||||
);
|
||||
--advanced-camera-card-ptz-color-selected: color-mix(
|
||||
in oklab,
|
||||
var(--advanced-camera-card-foreground-primary),
|
||||
transparent 20%
|
||||
);
|
||||
|
||||
/******************
|
||||
* Overlay Message
|
||||
|
||||
+8
-7
@@ -34,6 +34,7 @@ import {
|
||||
AdvancedCameraCardCustomActionConfig,
|
||||
} from '../config/schema/actions/types.js';
|
||||
import { AdvancedCameraCardUserSpecifiedView } from '../config/schema/common/const.js';
|
||||
import { PTZControlType } from '../config/schema/common/controls/ptz.js';
|
||||
import { ServiceCallRequest } from '../ha/types.js';
|
||||
import type { EffectName } from '../types.js';
|
||||
import { arrayify } from './basic.js';
|
||||
@@ -111,16 +112,16 @@ export function createDisplayModeAction(
|
||||
};
|
||||
}
|
||||
|
||||
export function createPTZControlsAction(
|
||||
enabled: boolean,
|
||||
options?: {
|
||||
cardID?: string;
|
||||
},
|
||||
): PTZControlsActionConfig {
|
||||
export function createPTZControlsAction(options?: {
|
||||
cardID?: string;
|
||||
enabled?: boolean;
|
||||
type?: PTZControlType;
|
||||
}): PTZControlsActionConfig {
|
||||
return {
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'ptz_controls',
|
||||
enabled: enabled,
|
||||
...(options?.enabled !== undefined && { enabled: options.enabled }),
|
||||
...(options?.type && { type: options.type }),
|
||||
...(options?.cardID && { card_id: options.cardID }),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user