Add out-of-the-box Frigate PTZ support.
This commit is contained in:
@@ -69,7 +69,14 @@ export class FrigateCardCarousel extends LitElement {
|
||||
this.setAttribute('direction', this.direction);
|
||||
}
|
||||
|
||||
const destroyProperties = ['direction', 'dragFree', 'transitionEffect'] as const;
|
||||
const destroyProperties = [
|
||||
'direction',
|
||||
'dragEnabled',
|
||||
'dragFree',
|
||||
'loop',
|
||||
'plugins',
|
||||
'transitionEffect',
|
||||
] as const;
|
||||
if (destroyProperties.some((prop) => changedProps.has(prop))) {
|
||||
this._carousel?.destroy();
|
||||
this._carousel = null;
|
||||
|
||||
+8
-128
@@ -1,19 +1,18 @@
|
||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import {
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
FrigateCardPTZConfig,
|
||||
ConditionsManagerEpoch,
|
||||
evaluateConditionViaEvent,
|
||||
} from '../card-controller/conditions-manager.js';
|
||||
import {
|
||||
FrigateConditional,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
@@ -23,19 +22,9 @@ import {
|
||||
PictureElements,
|
||||
} from '../config/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import ptzStyle from '../scss/elements-ptz.scss';
|
||||
import elementsStyle from '../scss/elements.scss';
|
||||
import { FrigateCardError } from '../types.js';
|
||||
import {
|
||||
frigateCardHandleActionConfig,
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import {
|
||||
ConditionsManagerEpoch,
|
||||
evaluateConditionViaEvent,
|
||||
} from '../card-controller/conditions-manager.js';
|
||||
import { dispatchFrigateCardEvent, errorToConsole } from '../utils/basic.js';
|
||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||
|
||||
/* A note on picture element rendering:
|
||||
@@ -122,7 +111,7 @@ export class FrigateCardElementsCore extends LitElement {
|
||||
try {
|
||||
element.setConfig(config);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
errorToConsole(e as Error, console.error);
|
||||
throw new FrigateCardError(localize('error.invalid_elements_config'));
|
||||
}
|
||||
return element;
|
||||
@@ -342,114 +331,6 @@ export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuI
|
||||
@customElement('frigate-card-menu-submenu-select')
|
||||
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
|
||||
|
||||
@customElement('frigate-card-ptz')
|
||||
export class FrigateCardPTZ extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@state()
|
||||
protected _config: FrigateCardPTZConfig | null = null;
|
||||
|
||||
/**
|
||||
* Set the card config.
|
||||
* @param config The configuration.
|
||||
*/
|
||||
public setConfig(config: FrigateCardPTZConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before each update.
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('_config')) {
|
||||
this.setAttribute('data-orientation', this._config?.orientation ?? 'vertical');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a PTZ action.
|
||||
* @param ev The actionHandler event.
|
||||
* @param config The action configuration.
|
||||
*/
|
||||
protected _actionHandler(
|
||||
ev: HASSDomEvent<{ action: string }>,
|
||||
config?: ActionsConfig,
|
||||
): 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, this.hass, config, interaction, action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the elements.
|
||||
* @returns A rendered template or void.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const renderIcon = (
|
||||
name: string,
|
||||
icon: string,
|
||||
actions?: Actions,
|
||||
): TemplateResult => {
|
||||
const hasHold = frigateCardHasAction(actions?.hold_action);
|
||||
const hasDoubleClick = frigateCardHasAction(actions?.double_tap_action);
|
||||
const classes = {
|
||||
[name]: true,
|
||||
disabled: !actions,
|
||||
};
|
||||
|
||||
return html`<ha-icon
|
||||
class=${classMap(classes)}
|
||||
icon=${icon}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasHold,
|
||||
hasDoubleClick: hasDoubleClick,
|
||||
})}
|
||||
.title=${localize(`elements.ptz.${name}`)}
|
||||
@action=${(ev) => this._actionHandler(ev, actions)}
|
||||
></ha-icon>`;
|
||||
};
|
||||
|
||||
return html` <div class="ptz">
|
||||
<div class="ptz-move">
|
||||
${renderIcon('right', 'mdi:arrow-right', this._config.actions_right)}
|
||||
${renderIcon('left', 'mdi:arrow-left', this._config.actions_left)}
|
||||
${renderIcon('up', 'mdi:arrow-up', this._config.actions_up)}
|
||||
${renderIcon('down', 'mdi:arrow-down', this._config.actions_down)}
|
||||
</div>
|
||||
${this._config.actions_zoom_in || this._config.actions_zoom_out
|
||||
? html` <div class="ptz-zoom">
|
||||
${renderIcon('zoom_in', 'mdi:plus', this._config.actions_zoom_in)}
|
||||
${renderIcon('zoom_out', 'mdi:minus', this._config.actions_zoom_out)}
|
||||
</div>`
|
||||
: html``}
|
||||
${this._config.actions_home
|
||||
? html`
|
||||
<div class="ptz-home">
|
||||
${renderIcon('home', 'mdi:home', this._config.actions_home)}
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return compiled CSS styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(ptzStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-conditional': FrigateCardElementsConditional;
|
||||
@@ -459,6 +340,5 @@ declare global {
|
||||
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
|
||||
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
|
||||
'frigate-card-elements-core': FrigateCardElementsCore;
|
||||
'frigate-card-ptz': FrigateCardPTZ;
|
||||
}
|
||||
}
|
||||
|
||||
+47
-39
@@ -13,7 +13,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||
import {
|
||||
ConditionsManagerEpoch,
|
||||
getOverriddenConfig,
|
||||
@@ -45,6 +45,7 @@ import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy
|
||||
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
||||
import {
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
@@ -55,18 +56,20 @@ import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { FrigateCardPTZ } from '../ptz.js';
|
||||
import '../surround.js';
|
||||
import '../title-control.js';
|
||||
import {
|
||||
FrigateCardTitleControl,
|
||||
getDefaultTitleConfigForView,
|
||||
} from '../title-control.js';
|
||||
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
||||
|
||||
interface LiveViewContext {
|
||||
// A cameraID override (used for dependencies/substreams to force a different
|
||||
// camera to be live rather than the camera selected in the view).
|
||||
overrides?: Map<string, string>;
|
||||
ptzVisible?: boolean;
|
||||
}
|
||||
|
||||
declare module 'view' {
|
||||
@@ -387,6 +390,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
// Index between camera name and slide number.
|
||||
protected _cameraToSlide: Record<string, number> = {};
|
||||
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
|
||||
protected _refPTZControl: Ref<FrigateCardPTZ> = createRef();
|
||||
|
||||
@state()
|
||||
protected _mediaHasLoaded = false;
|
||||
|
||||
protected _getTransitionEffect(): TransitionEffect {
|
||||
return (
|
||||
@@ -449,25 +456,20 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||
let cameras: CameraConfigs | null = null;
|
||||
if (this.viewFilterCameraID) {
|
||||
const config = this.cameraManager
|
||||
?.getStore()
|
||||
.getCameraConfig(this.viewFilterCameraID);
|
||||
if (config) {
|
||||
cameras = new Map([[this.viewFilterCameraID, config]]);
|
||||
}
|
||||
} else {
|
||||
cameras = this.cameraManager?.getStore().getVisibleCameras() ?? null;
|
||||
}
|
||||
if (!cameras) {
|
||||
if (!this.cameraManager) {
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
const cameraIDs = this.viewFilterCameraID
|
||||
? new Set([this.viewFilterCameraID])
|
||||
: this.cameraManager.getStore().getVisibleCameraIDs();
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
|
||||
for (const [cameraID, cameraConfig] of cameras) {
|
||||
for (const [cameraID, cameraConfig] of this.cameraManager
|
||||
.getStore()
|
||||
.getCameraConfigEntries(cameraIDs)) {
|
||||
const liveCameraID =
|
||||
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||
const liveCameraConfig =
|
||||
@@ -487,9 +489,9 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (cameraIDs && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||
this._setViewCameraID([...cameraIDs][ev.detail.index]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,22 +577,23 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (this.viewFilterCameraID || !cameras || !this.view || !this.hass) {
|
||||
const cameraIDs = this.cameraManager
|
||||
? [...this.cameraManager.getStore().getVisibleCameraIDs()]
|
||||
: [];
|
||||
if (this.viewFilterCameraID || cameraIDs.length <= 1 || !this.view || !this.hass) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
const cameraID = this.viewFilterCameraID ?? this.view.camera;
|
||||
const keys = Array.from(cameras.keys());
|
||||
const currentIndex = keys.indexOf(cameraID);
|
||||
const currentIndex = cameraIDs.indexOf(cameraID);
|
||||
|
||||
if (currentIndex < 0 || cameras.size <= 1) {
|
||||
if (currentIndex < 0) {
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
return [
|
||||
keys[currentIndex > 0 ? currentIndex - 1 : cameras.size - 1],
|
||||
keys[currentIndex + 1 < cameras.size ? currentIndex + 1 : 0],
|
||||
cameraIDs[currentIndex > 0 ? currentIndex - 1 : cameraIDs.length - 1],
|
||||
cameraIDs[currentIndex + 1 < cameraIDs.length ? currentIndex + 1 : 0],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -608,18 +611,19 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
const hasMultipleCameras = slides.length > 1;
|
||||
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
|
||||
|
||||
const overrideCameraID = (cameraID: string): string => {
|
||||
const getOverrideCameraID = (cameraID: string): string => {
|
||||
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||
};
|
||||
|
||||
const cameraMetadataPrevious = prevID
|
||||
? this.cameraManager.getCameraMetadata(overrideCameraID(prevID))
|
||||
? this.cameraManager.getCameraMetadata(getOverrideCameraID(prevID))
|
||||
: null;
|
||||
const cameraID = this.viewFilterCameraID ?? this.view.camera;
|
||||
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
||||
overrideCameraID(this.viewFilterCameraID ?? this.view.camera),
|
||||
getOverrideCameraID(cameraID),
|
||||
);
|
||||
const cameraMetadataNext = nextID
|
||||
? this.cameraManager.getCameraMetadata(overrideCameraID(nextID))
|
||||
? this.cameraManager.getCameraMetadata(getOverrideCameraID(nextID))
|
||||
: null;
|
||||
|
||||
const titleConfig = getDefaultTitleConfigForView(
|
||||
@@ -656,6 +660,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
if (this._refTitleControl.value) {
|
||||
this._refTitleControl.value.show();
|
||||
}
|
||||
this._mediaHasLoaded = true;
|
||||
}}
|
||||
@frigate-card:media:unloaded=${() => {
|
||||
this._mediaHasLoaded = false;
|
||||
}}
|
||||
>
|
||||
<frigate-card-next-previous-control
|
||||
@@ -688,6 +696,14 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
>
|
||||
</frigate-card-next-previous-control>
|
||||
</frigate-card-carousel>
|
||||
<frigate-card-ptz
|
||||
.hass=${this.hass}
|
||||
.config=${this.overriddenLiveConfig.controls.ptz}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cameraID=${cameraID}
|
||||
.forceVisibility=${this._mediaHasLoaded && this.view.context?.live?.ptzVisible}
|
||||
>
|
||||
</frigate-card-ptz>
|
||||
${cameraMetadataCurrent && titleConfig
|
||||
? html`<frigate-card-title-control
|
||||
${ref(this._refTitleControl)}
|
||||
@@ -846,9 +862,6 @@ export class FrigateCardLiveProvider
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
this._isVideoMediaLoaded = false;
|
||||
}
|
||||
@@ -860,9 +873,6 @@ export class FrigateCardLiveProvider
|
||||
this._isVideoMediaLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before each update.
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('load')) {
|
||||
if (!this.load) {
|
||||
@@ -915,10 +925,6 @@ export class FrigateCardLiveProvider
|
||||
: template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.load || !this.hass || !this.liveConfig || !this.cameraConfig) {
|
||||
return;
|
||||
@@ -941,6 +947,8 @@ export class FrigateCardLiveProvider
|
||||
return;
|
||||
}
|
||||
if (stateObj.state === 'unavailable') {
|
||||
dispatchMediaUnloadedEvent(this);
|
||||
|
||||
// An unavailable camera gets a message rendered in place vs dispatched,
|
||||
// as this may be a common occurrence (e.g. Frigate cameras that stop
|
||||
// receiving frames). Otherwise a single temporarily unavailable camera
|
||||
|
||||
@@ -167,8 +167,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_ev: CustomEvent<{ value: unknown }>,
|
||||
): Promise<void> {
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!this.hass || !cameras || !this.cameraManager || !this.view) {
|
||||
const visibleCameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (!this.hass || !visibleCameraIDs || !this.cameraManager || !this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
};
|
||||
|
||||
const cameraIDs =
|
||||
getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(cameras.keys());
|
||||
getArrayValueAsSet(this._refCamera.value?.value) ?? visibleCameraIDs;
|
||||
const mediaType = this._refMediaType.value?.value as
|
||||
| MediaFilterMediaType
|
||||
| undefined;
|
||||
@@ -268,9 +268,9 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('cameraManager')) {
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (cameras) {
|
||||
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
||||
this._cameraOptions = [...cameras].map((cameraID) => ({
|
||||
value: cameraID,
|
||||
label: this.hass
|
||||
? this.cameraManager?.getCameraMetadata(cameraID)?.title ?? ''
|
||||
@@ -319,8 +319,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
|
||||
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
|
||||
const queries = this.view?.query?.getQueries();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!this.view || !queries || !cameras) {
|
||||
const visibleCameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (!this.view || !queries || !visibleCameraIDs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
);
|
||||
// Special note: If all visible cameras are selected, this is the same as no
|
||||
// selector at all.
|
||||
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, cameras)) {
|
||||
if (cameraIDSets.length === 1 && !isEqual(queries[0].cameraIDs, visibleCameraIDs)) {
|
||||
cameraIDs = [...queries[0].cameraIDs];
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export class FrigateCardMenu extends LitElement {
|
||||
* @param action The action to check.
|
||||
* @returns `true` if the action toggles the menu, `false` otherwise.
|
||||
*/
|
||||
protected _isMenuToggleAction(action: ActionType | undefined): boolean {
|
||||
protected _isMenuToggleAction(action: ActionType | null): boolean {
|
||||
// Determine if this action is a Frigate card action, if so handle it
|
||||
// internally.
|
||||
if (!action) {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { HASSDomEvent, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { PTZController } from '../components-lib/ptz-controller.js';
|
||||
import { Actions, FrigateCardPTZConfig } from '../config/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import ptzStyle from '../scss/ptz.scss';
|
||||
import { frigateCardHasAction } from '../utils/action.js';
|
||||
|
||||
@customElement('frigate-card-ptz')
|
||||
export class FrigateCardPTZ extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: FrigateCardPTZConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraID?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public forceVisibility?: boolean;
|
||||
|
||||
protected _controller = new PTZController(this);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('config')) {
|
||||
this._controller.setConfig(this.config);
|
||||
}
|
||||
if (changedProps.has('hass')) {
|
||||
this._controller.setHASS(this.hass);
|
||||
}
|
||||
if (changedProps.has('cameraManager') || changedProps.has('cameraID')) {
|
||||
this._controller.setCamera(this.cameraManager, this.cameraID);
|
||||
}
|
||||
if (changedProps.has('forceVisibility')) {
|
||||
this._controller.setForceVisibility(this.forceVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._controller.shouldDisplay()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderIcon = (
|
||||
name: string,
|
||||
icon: string,
|
||||
actions: Actions | null,
|
||||
): TemplateResult => {
|
||||
const classes = {
|
||||
[name]: true,
|
||||
disabled: !actions,
|
||||
};
|
||||
|
||||
return html`<ha-icon
|
||||
class=${classMap(classes)}
|
||||
icon=${icon}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: frigateCardHasAction(actions?.hold_action),
|
||||
hasDoubleClick: frigateCardHasAction(actions?.double_tap_action),
|
||||
})}
|
||||
.title=${localize(`elements.ptz.${name}`)}
|
||||
@action=${(ev: HASSDomEvent<{ action: string }>) =>
|
||||
this._controller.handleAction(ev, actions)}
|
||||
></ha-icon>`;
|
||||
};
|
||||
|
||||
const config = this._controller.getConfig();
|
||||
const actionsZoomIn = this._controller.getPTZActions('zoom_in');
|
||||
const actionsZoomOut = this._controller.getPTZActions('zoom_out');
|
||||
const actionsHome = this._controller.getPTZActions('home');
|
||||
|
||||
return html` <div class="ptz">
|
||||
${!config?.hide_pan_tilt
|
||||
? html`<div class="ptz-move">
|
||||
${renderIcon(
|
||||
'right',
|
||||
'mdi:arrow-right',
|
||||
this._controller.getPTZActions('right'),
|
||||
)}
|
||||
${renderIcon(
|
||||
'left',
|
||||
'mdi:arrow-left',
|
||||
this._controller.getPTZActions('left'),
|
||||
)}
|
||||
${renderIcon('up', 'mdi:arrow-up', this._controller.getPTZActions('up'))}
|
||||
${renderIcon(
|
||||
'down',
|
||||
'mdi:arrow-down',
|
||||
this._controller.getPTZActions('down'),
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
${!config?.hide_zoom && (actionsZoomIn || actionsZoomOut)
|
||||
? html` <div class="ptz-zoom">
|
||||
${renderIcon('zoom_in', 'mdi:plus', actionsZoomIn)}
|
||||
${renderIcon('zoom_out', 'mdi:minus', actionsZoomOut)}
|
||||
</div>`
|
||||
: html``}
|
||||
${!config?.hide_home && actionsHome
|
||||
? html`
|
||||
<div class="ptz-home">${renderIcon('home', 'mdi:home', actionsHome)}</div>
|
||||
`
|
||||
: html``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(ptzStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-ptz': FrigateCardPTZ;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { ClipsOrSnapshotsOrAll, ExtendedHomeAssistant } from '../types.js';
|
||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { getAllDependentCameras } from '../utils/camera.js';
|
||||
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
||||
import { View } from '../view/view.js';
|
||||
import './surround-basic.js';
|
||||
@@ -140,7 +139,8 @@ export class FrigateCardSurround extends LitElement {
|
||||
if (this.view?.is('live')) {
|
||||
return this.view.isGrid()
|
||||
? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
||||
: getAllDependentCameras(this.cameraManager, this.view.camera);
|
||||
: this.cameraManager?.getStore().getAllDependentCameras(this.view.camera) ??
|
||||
null;
|
||||
}
|
||||
if (this.view.isViewerView()) {
|
||||
return this.view.query?.getQueryCameraIDs() ?? null;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ExtendedHomeAssistant } from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
||||
import AutoSize from '../utils/embla/plugins/auto-size/auto-size.js';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { View } from '../view/view.js';
|
||||
import './carousel.js';
|
||||
@@ -136,6 +137,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
|
||||
return html`<frigate-card-carousel
|
||||
direction=${direction}
|
||||
.plugins=${[AutoSize()]}
|
||||
.selected=${this._getSelectedSlide() ?? 0}
|
||||
.dragFree=${true}
|
||||
>
|
||||
|
||||
+48
-29
@@ -9,10 +9,17 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { ConditionsManagerEpoch, getOverridesByKey } from '../card-controller/conditions-manager.js';
|
||||
import {
|
||||
ConditionsManagerEpoch,
|
||||
getOverridesByKey,
|
||||
} from '../card-controller/conditions-manager.js';
|
||||
import {
|
||||
CardWideConfig,
|
||||
FrigateCardConfig,
|
||||
RawFrigateCardConfig,
|
||||
} from '../config/types.js';
|
||||
import viewsStyle from '../scss/views.scss';
|
||||
import { ExtendedHomeAssistant } from '../types.js';
|
||||
import { ConfigManager } from '../card-controller/config-manager.js';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||
import { View } from '../view/view.js';
|
||||
import './surround.js';
|
||||
@@ -32,7 +39,16 @@ export class FrigateCardViews extends LitElement {
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public configManager?: ConfigManager;
|
||||
public nonOverriddenConfig?: FrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public overriddenConfig?: FrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public rawConfig?: RawFrigateCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
@@ -94,21 +110,21 @@ export class FrigateCardViews extends LitElement {
|
||||
return (
|
||||
// Special case: Never preload for diagnostics -- we want that to be as
|
||||
// minimal as possible.
|
||||
!!this.configManager?.getConfig()?.live.preload && !this.view?.is('diagnostics')
|
||||
!!this.overriddenConfig?.live.preload && !this.view?.is('diagnostics')
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const config = this.configManager?.getConfig();
|
||||
const nonOverriddenConfig = this.configManager?.getNonOverriddenConfig();
|
||||
const cardWideConfig = this.configManager?.getCardWideConfig();
|
||||
const rawConfig = this.configManager?.getRawConfig();
|
||||
|
||||
// Only essential items should be added to the below list, since we want the
|
||||
// overall views pane to render in ~almost all cases (e.g. for a camera
|
||||
// initialization error to display, `view` and `cameraConfig` may both be
|
||||
// undefined, but we still want to render).
|
||||
if (!this.hass || !config || !nonOverriddenConfig || !cardWideConfig) {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.overriddenConfig ||
|
||||
!this.nonOverriddenConfig ||
|
||||
!this.cardWideConfig
|
||||
) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -122,17 +138,17 @@ export class FrigateCardViews extends LitElement {
|
||||
};
|
||||
|
||||
const thumbnailConfig = this.view?.is('live')
|
||||
? config.live.controls.thumbnails
|
||||
? this.overriddenConfig.live.controls.thumbnails
|
||||
: this.view?.isViewerView()
|
||||
? config.media_viewer.controls.thumbnails
|
||||
? this.overriddenConfig.media_viewer.controls.thumbnails
|
||||
: this.view?.is('timeline')
|
||||
? config.timeline.controls.thumbnails
|
||||
? this.overriddenConfig.timeline.controls.thumbnails
|
||||
: undefined;
|
||||
|
||||
const miniTimelineConfig = this.view?.is('live')
|
||||
? config.live.controls.timeline
|
||||
? this.overriddenConfig.live.controls.timeline
|
||||
: this.view?.isViewerView()
|
||||
? config.media_viewer.controls.timeline
|
||||
? this.overriddenConfig.media_viewer.controls.timeline
|
||||
: undefined;
|
||||
|
||||
const cameraConfig = this.view
|
||||
@@ -144,16 +160,16 @@ export class FrigateCardViews extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.fetchMedia=${this.view?.is('live')
|
||||
? config.live.controls.thumbnails.media
|
||||
? this.overriddenConfig.live.controls.thumbnails.media
|
||||
: undefined}
|
||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${cardWideConfig}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
${!this.hide && this.view?.is('image') && cameraConfig
|
||||
? html` <frigate-card-image
|
||||
.imageConfig=${config.image}
|
||||
.imageConfig=${this.overriddenConfig.image}
|
||||
.view=${this.view}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${cameraConfig}
|
||||
@@ -165,9 +181,9 @@ export class FrigateCardViews extends LitElement {
|
||||
? html` <frigate-card-gallery
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.galleryConfig=${config.media_gallery}
|
||||
.galleryConfig=${this.overriddenConfig.media_gallery}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${cardWideConfig}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-gallery>`
|
||||
: ``}
|
||||
@@ -176,10 +192,10 @@ export class FrigateCardViews extends LitElement {
|
||||
<frigate-card-viewer
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.viewerConfig=${config.media_viewer}
|
||||
.viewerConfig=${this.overriddenConfig.media_viewer}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${cardWideConfig}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-viewer>
|
||||
`
|
||||
@@ -188,16 +204,16 @@ export class FrigateCardViews extends LitElement {
|
||||
? html` <frigate-card-timeline
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.timelineConfig=${config.timeline}
|
||||
.timelineConfig=${this.overriddenConfig.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${cardWideConfig}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</frigate-card-timeline>`
|
||||
: ``}
|
||||
${!this.hide && this.view?.is('diagnostics')
|
||||
? html` <frigate-card-diagnostics
|
||||
.hass=${this.hass}
|
||||
.rawConfig=${rawConfig}
|
||||
.rawConfig=${this.rawConfig}
|
||||
>
|
||||
</frigate-card-diagnostics>`
|
||||
: ``}
|
||||
@@ -213,12 +229,15 @@ export class FrigateCardViews extends LitElement {
|
||||
<frigate-card-live
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.nonOverriddenLiveConfig=${nonOverriddenConfig.live}
|
||||
.overriddenLiveConfig=${config.live}
|
||||
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
|
||||
.overriddenLiveConfig=${this.overriddenConfig.live}
|
||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||
.liveOverrides=${getOverridesByKey('live', config.overrides)}
|
||||
.liveOverrides=${getOverridesByKey(
|
||||
'live',
|
||||
this.overriddenConfig.overrides,
|
||||
)}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${cardWideConfig}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.microphoneStream=${this.microphoneStream}
|
||||
class="${classMap(liveClasses)}"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user