Merge pull request #1070 from dermotduffy/click-always-plays
Allow smooth panning between past & present
This commit is contained in:
@@ -134,6 +134,9 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
|
|||||||
// progress.
|
// progress.
|
||||||
return !this.getEndTime();
|
return !this.getEndTime();
|
||||||
}
|
}
|
||||||
|
public getVideoContentType(): VideoContentType | null {
|
||||||
|
return VideoContentType.HLS;
|
||||||
|
}
|
||||||
public getContentID(): string | null {
|
public getContentID(): string | null {
|
||||||
return this._contentID;
|
return this._contentID;
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-139
@@ -5,63 +5,67 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import merge from 'lodash-es/merge';
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
|
import { ViewContext } from 'view';
|
||||||
|
import 'web-dialog';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import pkg from '../package.json';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
|
import { CameraManagerEngineFactory } from './camera-manager/engine-factory.js';
|
||||||
|
import { CameraManager } from './camera-manager/manager.js';
|
||||||
import {
|
import {
|
||||||
CardConditionManager,
|
CardConditionManager,
|
||||||
ConditionState,
|
ConditionState,
|
||||||
conditionStateRequestHandler,
|
conditionStateRequestHandler,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig
|
||||||
getOverridesByKey,
|
|
||||||
} from './card-condition.js';
|
} from './card-condition.js';
|
||||||
import './components/elements.js';
|
import './components/elements.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import type { FrigateCardImage } from './components/image.js';
|
|
||||||
import type { FrigateCardLive } from './components/live/live.js';
|
|
||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js';
|
import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
||||||
import './components/thumbnail-carousel.js';
|
import './components/thumbnail-carousel.js';
|
||||||
|
import './components/views.js';
|
||||||
|
import { FrigateCardViews } from './components/views.js';
|
||||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, REPO_URL } from './const.js';
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, REPO_URL } from './const.js';
|
||||||
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
|
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
|
||||||
|
import { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js';
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant, FrigateCardConfig,
|
||||||
FRIGATE_CARD_VIEW_DEFAULT,
|
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
FrigateCardConfig,
|
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
FrigateCardError,
|
FrigateCardError,
|
||||||
FrigateCardView,
|
FrigateCardView, FRIGATE_CARD_VIEWS_USER_SPECIFIED, FRIGATE_CARD_VIEW_DEFAULT, MediaLoadedInfo,
|
||||||
MediaLoadedInfo,
|
MenuButton, Message, MESSAGE_TYPE_PRIORITIES, RawFrigateCardConfig
|
||||||
MenuButton,
|
|
||||||
MESSAGE_TYPE_PRIORITIES,
|
|
||||||
Message,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
createFrigateCardCustomAction,
|
createFrigateCardCustomAction,
|
||||||
frigateCardHandleAction,
|
frigateCardHandleAction,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction
|
||||||
} from './utils/action.js';
|
} from './utils/action.js';
|
||||||
import { errorToConsole } from './utils/basic.js';
|
import { errorToConsole } from './utils/basic.js';
|
||||||
|
import { getAllDependentCameras } from './utils/camera.js';
|
||||||
|
import { log } from './utils/debug.js';
|
||||||
|
import { downloadMedia } from './utils/download.js';
|
||||||
import {
|
import {
|
||||||
getEntityIcon,
|
getEntityIcon,
|
||||||
getEntityTitle,
|
getEntityTitle,
|
||||||
@@ -69,30 +73,18 @@ import {
|
|||||||
isCardInPanel,
|
isCardInPanel,
|
||||||
isHassDifferent,
|
isHassDifferent,
|
||||||
isTriggeredState,
|
isTriggeredState,
|
||||||
sideLoadHomeAssistantElements,
|
sideLoadHomeAssistantElements
|
||||||
} from './utils/ha';
|
} from './utils/ha';
|
||||||
import { DeviceList, getAllDevices } from './utils/ha/device-registry.js';
|
import { DeviceList, getAllDevices } from './utils/ha/device-registry.js';
|
||||||
|
import { EntityCache } from './utils/ha/entity-registry/cache.js';
|
||||||
|
import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
|
||||||
|
import { Entity } from './utils/ha/entity-registry/types.js';
|
||||||
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
||||||
import { supportsFeature } from './utils/ha/update.js';
|
import { supportsFeature } from './utils/ha/update.js';
|
||||||
import { isValidMediaLoadedInfo } from './utils/media-info.js';
|
|
||||||
import { View } from './view/view.js';
|
|
||||||
import pkg from '../package.json';
|
|
||||||
import { ViewContext } from 'view';
|
|
||||||
import { CameraManager } from './camera-manager/manager.js';
|
|
||||||
import { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js';
|
|
||||||
import { CameraManagerEngineFactory } from './camera-manager/engine-factory.js';
|
|
||||||
import { log } from './utils/debug.js';
|
|
||||||
import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
|
|
||||||
import { EntityCache } from './utils/ha/entity-registry/cache.js';
|
|
||||||
import { Entity } from './utils/ha/entity-registry/types.js';
|
|
||||||
import { getAllDependentCameras } from './utils/camera.js';
|
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
|
||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import merge from 'lodash-es/merge';
|
|
||||||
import { FrigateCardInitializer } from './utils/initializer.js';
|
import { FrigateCardInitializer } from './utils/initializer.js';
|
||||||
import 'web-dialog';
|
import { isValidMediaLoadedInfo } from './utils/media-info.js';
|
||||||
import { downloadMedia } from './utils/download.js';
|
|
||||||
import { getActionsFromQueryString } from './utils/querystring.js';
|
import { getActionsFromQueryString } from './utils/querystring.js';
|
||||||
|
import { View } from './view/view.js';
|
||||||
|
|
||||||
/** A note on media callbacks:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
@@ -185,8 +177,7 @@ class FrigateCard extends LitElement {
|
|||||||
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
||||||
protected _refMain: Ref<HTMLElement> = createRef();
|
protected _refMain: Ref<HTMLElement> = createRef();
|
||||||
protected _refElements: Ref<FrigateCardElements> = createRef();
|
protected _refElements: Ref<FrigateCardElements> = createRef();
|
||||||
protected _refImage: Ref<FrigateCardImage> = createRef();
|
protected _refViews: Ref<FrigateCardViews> = createRef();
|
||||||
protected _refLive: Ref<FrigateCardLive> = createRef();
|
|
||||||
|
|
||||||
// user interaction timer ("screensaver" functionality, return to default
|
// user interaction timer ("screensaver" functionality, return to default
|
||||||
// view after user interaction).
|
// view after user interaction).
|
||||||
@@ -246,8 +237,8 @@ class FrigateCard extends LitElement {
|
|||||||
if (this._refElements.value) {
|
if (this._refElements.value) {
|
||||||
this._refElements.value.hass = this._hass;
|
this._refElements.value.hass = this._hass;
|
||||||
}
|
}
|
||||||
if (this._refImage.value) {
|
if (this._refViews.value) {
|
||||||
this._refImage.value.hass = this._hass;
|
this._refViews.value.hass = this._hass;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,8 +302,8 @@ class FrigateCard extends LitElement {
|
|||||||
// to them to avoid the performance hit of a entire card re-render (esp.
|
// to them to avoid the performance hit of a entire card re-render (esp.
|
||||||
// when using card-mod).
|
// when using card-mod).
|
||||||
// https://github.com/dermotduffy/frigate-hass-card/issues/678
|
// https://github.com/dermotduffy/frigate-hass-card/issues/678
|
||||||
if (this._refLive.value) {
|
if (this._refViews.value) {
|
||||||
this._refLive.value.conditionState = this._conditionState;
|
this._refViews.value.conditionState = this._conditionState;
|
||||||
}
|
}
|
||||||
if (this._refElements.value) {
|
if (this._refElements.value) {
|
||||||
this._refElements.value.conditionState = this._conditionState;
|
this._refElements.value.conditionState = this._conditionState;
|
||||||
@@ -947,18 +938,6 @@ class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
this._initializeBackground();
|
this._initializeBackground();
|
||||||
|
|
||||||
if (this._view?.is('live')) {
|
|
||||||
import('./components/live/live.js');
|
|
||||||
} else if (this._view?.isGalleryView()) {
|
|
||||||
import('./components/gallery.js');
|
|
||||||
} else if (this._view?.isViewerView()) {
|
|
||||||
import('./components/viewer.js');
|
|
||||||
} else if (this._view?.is('image')) {
|
|
||||||
import('./components/image.js');
|
|
||||||
} else if (this._view?.is('timeline')) {
|
|
||||||
import('./components/timeline.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedProps.has('_view')) {
|
if (changedProps.has('_view')) {
|
||||||
this._setPropertiesForExpandedMode();
|
this._setPropertiesForExpandedMode();
|
||||||
}
|
}
|
||||||
@@ -2013,7 +1992,18 @@ class FrigateCard extends LitElement {
|
|||||||
? renderProgressIndicator({ cardWideConfig: this._cardWideConfig })
|
? renderProgressIndicator({ cardWideConfig: this._cardWideConfig })
|
||||||
: // Always want to call render even if there's a message, to
|
: // Always want to call render even if there's a message, to
|
||||||
// ensure live preload is always present (even if not displayed).
|
// ensure live preload is always present (even if not displayed).
|
||||||
this._render()}
|
html`<frigate-card-views
|
||||||
|
${ref(this._refViews)}
|
||||||
|
.hass=${this._hass}
|
||||||
|
.view=${this._view}
|
||||||
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
|
.cameraManager=${this._cameraManager}
|
||||||
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
|
.config=${this._getConfig()}
|
||||||
|
.nonOverriddenConfig=${this._config}
|
||||||
|
.conditionState=${this._conditionState}
|
||||||
|
.hide=${!!this._message}
|
||||||
|
></frigate-card-views>`}
|
||||||
${
|
${
|
||||||
// Keep message rendering to last to show messages that may have been
|
// Keep message rendering to last to show messages that may have been
|
||||||
// generated during the render.
|
// generated during the render.
|
||||||
@@ -2044,92 +2034,6 @@ class FrigateCard extends LitElement {
|
|||||||
</ha-card>`);
|
</ha-card>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sub-render method for the card.
|
|
||||||
*/
|
|
||||||
protected _render(): TemplateResult | void {
|
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
|
||||||
|
|
||||||
if (!this._hass || !this._view || !cameraConfig) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render but hide the live view if there's a message, or if it's preload
|
|
||||||
// mode and the view is not live.
|
|
||||||
const liveClasses = {
|
|
||||||
hidden:
|
|
||||||
!!this._message || (this._getConfig().live.preload && !this._view.is('live')),
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${!this._message && this._view.is('image')
|
|
||||||
? html` <frigate-card-image
|
|
||||||
${ref(this._refImage)}
|
|
||||||
.imageConfig=${this._getConfig().image}
|
|
||||||
.view=${this._view}
|
|
||||||
.hass=${this._hass}
|
|
||||||
.cameraConfig=${cameraConfig}
|
|
||||||
>
|
|
||||||
</frigate-card-image>`
|
|
||||||
: ``}
|
|
||||||
${!this._message && this._view.isGalleryView()
|
|
||||||
? html` <frigate-card-gallery
|
|
||||||
.hass=${this._hass}
|
|
||||||
.view=${this._view}
|
|
||||||
.galleryConfig=${this._getConfig().media_gallery}
|
|
||||||
.cameraManager=${this._cameraManager}
|
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
|
||||||
>
|
|
||||||
</frigate-card-gallery>`
|
|
||||||
: ``}
|
|
||||||
${!this._message && this._view.isViewerView()
|
|
||||||
? html` <frigate-card-viewer
|
|
||||||
.hass=${this._hass}
|
|
||||||
.view=${this._view}
|
|
||||||
.viewerConfig=${this._getConfig().media_viewer}
|
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
|
||||||
.cameraManager=${this._cameraManager}
|
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
|
||||||
>
|
|
||||||
</frigate-card-viewer>`
|
|
||||||
: ``}
|
|
||||||
${!this._message && this._view.is('timeline')
|
|
||||||
? html` <frigate-card-timeline
|
|
||||||
.hass=${this._hass}
|
|
||||||
.view=${this._view}
|
|
||||||
.timelineConfig=${this._getConfig().timeline}
|
|
||||||
.cameraManager=${this._cameraManager}
|
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
|
||||||
>
|
|
||||||
</frigate-card-timeline>`
|
|
||||||
: ``}
|
|
||||||
${
|
|
||||||
// Note: Subtle difference in condition below vs the other views in order
|
|
||||||
// to always render the live view for live.preload mode.
|
|
||||||
|
|
||||||
// Note: <frigate-card-live> uses the underlying _config rather than the
|
|
||||||
// overriden config (via getConfig), as it does it's own overriding as
|
|
||||||
// part of the camera carousel.
|
|
||||||
this._getConfig().live.preload || (!this._message && this._view.is('live'))
|
|
||||||
? html`
|
|
||||||
<frigate-card-live
|
|
||||||
${ref(this._refLive)}
|
|
||||||
.hass=${this._hass}
|
|
||||||
.view=${this._view}
|
|
||||||
.liveConfig=${this._config.live}
|
|
||||||
.conditionState=${this._conditionState}
|
|
||||||
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
|
|
||||||
.cameraManager=${this._cameraManager}
|
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
|
||||||
class="${classMap(liveClasses)}"
|
|
||||||
>
|
|
||||||
</frigate-card-live>
|
|
||||||
`
|
|
||||||
: ``
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(): void {
|
protected firstUpdated(): void {
|
||||||
// Execute query string actions after first render is complete.
|
// Execute query string actions after first render is complete.
|
||||||
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action));
|
getActionsFromQueryString().forEach((action) => this._cardActionHandler(action));
|
||||||
|
|||||||
@@ -75,17 +75,17 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
return this._player?.video?.play();
|
return this._player?.video?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._player?.video?.pause();
|
this._player?.video?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
if (this._player?.video) {
|
if (this._player?.video) {
|
||||||
this._player.video.muted = true;
|
this._player.video.muted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
if (this._player?.video) {
|
if (this._player?.video) {
|
||||||
this._player.video.muted = false;
|
this._player.video.muted = false;
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
return this._player?.video.muted ?? true;
|
return this._player?.video.muted ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
if (this._player?.video) {
|
if (this._player?.video) {
|
||||||
this._player.video.currentTime = seconds;
|
this._player.video.currentTime = seconds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
return this._playerRef.value?.play();
|
return this._playerRef.value?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._playerRef.value?.pause();
|
this._playerRef.value?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
this._playerRef.value?.mute();
|
this._playerRef.value?.mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
this._playerRef.value?.unmute();
|
this._playerRef.value?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
return this._playerRef.value?.isMuted() ?? true;
|
return this._playerRef.value?.isMuted() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
this._playerRef.value?.seek(seconds);
|
this._playerRef.value?.seek(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
this._playing = true;
|
this._playing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._playing = false;
|
this._playing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public seek(_seconds: number): void {
|
public async seek(_seconds: number): Promise<void> {
|
||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
|
|
||||||
return html` <frigate-card-image
|
return html` <frigate-card-image
|
||||||
.imageConfig=${{
|
.imageConfig=${{
|
||||||
mode: this.cameraConfig.image.url ? 'url' as const : 'camera' as const,
|
mode: this.cameraConfig.image.url ? ('url' as const) : ('camera' as const),
|
||||||
refresh_seconds: this._playing ? this.cameraConfig.image.refresh_seconds : 0,
|
refresh_seconds: this._playing ? this.cameraConfig.image.refresh_seconds : 0,
|
||||||
url: this.cameraConfig.image.url,
|
url: this.cameraConfig.image.url,
|
||||||
// Don't need to pass layout options as FrigateCardLiveProvider has
|
// Don't need to pass layout options as FrigateCardLiveProvider has
|
||||||
|
|||||||
@@ -43,18 +43,18 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
return this._jsmpegVideoPlayer?.play();
|
return this._jsmpegVideoPlayer?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._jsmpegVideoPlayer?.stop();
|
this._jsmpegVideoPlayer?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
const player = this._jsmpegVideoPlayer?.player;
|
||||||
if (player) {
|
if (player) {
|
||||||
player.volume = 0;
|
player.volume = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
const player = this._jsmpegVideoPlayer?.player;
|
||||||
if (player) {
|
if (player) {
|
||||||
player.volume = 1;
|
player.volume = 1;
|
||||||
@@ -66,7 +66,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public seek(_seconds: number): void {
|
public async seek(_seconds: number): Promise<void> {
|
||||||
// JSMPEG does not support seeking.
|
// JSMPEG does not support seeking.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,18 +44,18 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
return this._getPlayer()?.play();
|
return this._getPlayer()?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._getPlayer()?.pause();
|
this._getPlayer()?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getPlayer();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.muted = true;
|
player.muted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getPlayer();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.muted = false;
|
player.muted = false;
|
||||||
@@ -66,7 +66,7 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
return this._getPlayer()?.muted ?? true;
|
return this._getPlayer()?.muted ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getPlayer();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.currentTime = seconds;
|
player.currentTime = seconds;
|
||||||
|
|||||||
+100
-67
@@ -69,6 +69,11 @@ declare module 'view' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LastMediaLoadedInfo {
|
||||||
|
mediaLoadedInfo: MediaLoadedInfo;
|
||||||
|
source: EventTarget;
|
||||||
|
}
|
||||||
|
|
||||||
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,9 +146,11 @@ export class FrigateCardLive extends LitElement {
|
|||||||
// foreground and background (in preload mode).
|
// foreground and background (in preload mode).
|
||||||
protected _intersectionObserver: IntersectionObserver;
|
protected _intersectionObserver: IntersectionObserver;
|
||||||
|
|
||||||
// MediaLoadedInfo object and message from the underlying live object. In the
|
// MediaLoadedInfo object and target from the underlying live object. In the
|
||||||
// case of pre-loading these may be propagated upwards later.
|
// case of pre-loading these may be propagated later (from the original
|
||||||
protected _backgroundMediaLoadedInfo: MediaLoadedInfo | null = null;
|
// source).
|
||||||
|
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
||||||
|
|
||||||
protected _messageReceivedPostRender = false;
|
protected _messageReceivedPostRender = false;
|
||||||
protected _renderKey = 0;
|
protected _renderKey = 0;
|
||||||
|
|
||||||
@@ -164,12 +171,17 @@ export class FrigateCardLive extends LitElement {
|
|||||||
if (
|
if (
|
||||||
!this._inBackground &&
|
!this._inBackground &&
|
||||||
!this._messageReceivedPostRender &&
|
!this._messageReceivedPostRender &&
|
||||||
this._backgroundMediaLoadedInfo
|
this._lastMediaLoadedInfo
|
||||||
) {
|
) {
|
||||||
// If this isn't being rendered in the background, the last render did not
|
// If this isn't being rendered in the background, the last render did not
|
||||||
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
||||||
dispatchExistingMediaLoadedInfoAsEvent(this, this._backgroundMediaLoadedInfo);
|
dispatchExistingMediaLoadedInfoAsEvent(
|
||||||
this._backgroundMediaLoadedInfo = null;
|
// Specifically dispatch the event "where it came from", as otherwise
|
||||||
|
// the intermediate layers (e.g. media-carousel which controls the title
|
||||||
|
// popups) will not re-receive the events.
|
||||||
|
this._lastMediaLoadedInfo.source,
|
||||||
|
this._lastMediaLoadedInfo.mediaLoadedInfo,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger a re-render which may be necessary if the prior render resulted
|
// Trigger a re-render which may be necessary if the prior render resulted
|
||||||
@@ -217,18 +229,10 @@ export class FrigateCardLive extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = getOverriddenConfig(
|
|
||||||
this.liveConfig,
|
|
||||||
this.liveOverrides,
|
|
||||||
this.conditionState,
|
|
||||||
) as LiveConfig;
|
|
||||||
|
|
||||||
// Notes:
|
// Notes:
|
||||||
// - See use of liveConfig and not config below -- the carousel will
|
// - See use of liveConfig and not config below -- the carousel will
|
||||||
// independently override the liveConfig to reflect the camera in the
|
// independently override the liveConfig to reflect the camera in the
|
||||||
// carousel (not necessarily the selected camera).
|
// carousel (not necessarily the selected camera).
|
||||||
// - Fetching of thumbnails is disabled as long as live view is the
|
|
||||||
// background.
|
|
||||||
// - Various events are captured to prevent them propagating upwards if the
|
// - Various events are captured to prevent them propagating upwards if the
|
||||||
// card is in the background.
|
// card is in the background.
|
||||||
// - The entire returned template is keyed to allow for the whole template
|
// - The entire returned template is keyed to allow for the whole template
|
||||||
@@ -236,34 +240,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
// is received when the card is in the background).
|
// is received when the card is in the background).
|
||||||
const result = html`${keyed(
|
const result = html`${keyed(
|
||||||
this._renderKey,
|
this._renderKey,
|
||||||
html`<frigate-card-surround
|
html`
|
||||||
.hass=${this.hass}
|
|
||||||
.view=${this.view}
|
|
||||||
.fetchMedia=${config.controls.thumbnails.media}
|
|
||||||
.thumbnailConfig=${config.controls.thumbnails}
|
|
||||||
.timelineConfig=${config.controls.timeline}
|
|
||||||
.cameraManager=${this.cameraManager}
|
|
||||||
.inBackground=${this._inBackground}
|
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
|
||||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
|
||||||
this._renderKey++;
|
|
||||||
this._messageReceivedPostRender = true;
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
|
||||||
if (this._inBackground) {
|
|
||||||
this._backgroundMediaLoadedInfo = ev.detail;
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<frigate-card-live-carousel
|
<frigate-card-live-carousel
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
@@ -273,9 +250,30 @@ export class FrigateCardLive extends LitElement {
|
|||||||
.liveOverrides=${this.liveOverrides}
|
.liveOverrides=${this.liveOverrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
|
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
||||||
|
this._renderKey++;
|
||||||
|
this._messageReceivedPostRender = true;
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
|
this._lastMediaLoadedInfo = {
|
||||||
|
source: ev.composedPath()[0],
|
||||||
|
mediaLoadedInfo: ev.detail,
|
||||||
|
};
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
</frigate-card-live-carousel>
|
</frigate-card-live-carousel>
|
||||||
</frigate-card-surround>`,
|
`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
this._messageReceivedPostRender = false;
|
this._messageReceivedPostRender = false;
|
||||||
@@ -327,18 +325,22 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
updated(changedProperties: PropertyValues): void {
|
updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
const frigateCardMediaCarousel = this._refMediaCarousel.value;
|
if (changedProperties.has('inBackground')) {
|
||||||
|
this.updateComplete.then(async () => {
|
||||||
if (frigateCardMediaCarousel && changedProperties.has('inBackground')) {
|
const frigateCardMediaCarousel = this._refMediaCarousel.value;
|
||||||
// If this has changed to be in the background (i.e. preloaded but not
|
if (frigateCardMediaCarousel) {
|
||||||
// visible) take the appropriate play/pause/mute/unmute actions.
|
await frigateCardMediaCarousel.updateComplete;
|
||||||
if (this.inBackground) {
|
// If this has changed to be in the background (i.e. preloaded but not
|
||||||
frigateCardMediaCarousel.autoPause();
|
// visible) take the appropriate play/pause/mute/unmute actions.
|
||||||
frigateCardMediaCarousel.autoMute();
|
if (this.inBackground) {
|
||||||
} else {
|
frigateCardMediaCarousel.autoPause();
|
||||||
frigateCardMediaCarousel.autoPlay();
|
frigateCardMediaCarousel.autoMute();
|
||||||
frigateCardMediaCarousel.autoUnmute();
|
} else {
|
||||||
}
|
frigateCardMediaCarousel.autoPlay();
|
||||||
|
frigateCardMediaCarousel.autoUnmute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,21 +724,41 @@ export class FrigateCardLiveProvider
|
|||||||
@state()
|
@state()
|
||||||
protected _isVideoMediaLoaded = false;
|
protected _isVideoMediaLoaded = false;
|
||||||
|
|
||||||
protected _refProvider: Ref<Element & FrigateCardMediaPlayer> = createRef();
|
protected _refProvider: Ref<LitElement & FrigateCardMediaPlayer> = createRef();
|
||||||
|
|
||||||
|
// A note on dynamic imports:
|
||||||
|
//
|
||||||
|
// We gather the dynamic live provider import promises and do not consider the
|
||||||
|
// update of the element complete until these imports have returned. Without
|
||||||
|
// this behavior calls to the media methods (e.g. `mute()`) may throw if the
|
||||||
|
// underlying code is not yet loaded.
|
||||||
|
//
|
||||||
|
// Test case: A card with a non-live view, but live pre-loaded, attempts to
|
||||||
|
// call mute() when the <frigate-card-live> element first renders in the
|
||||||
|
// background. These calls fail without waiting for loading here.
|
||||||
|
protected _importPromises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async play(): Promise<void> {
|
||||||
playMediaMutingIfNecessary(this, this._refProvider.value);
|
await this.updateComplete;
|
||||||
|
await this._refProvider.value?.updateComplete;
|
||||||
|
await playMediaMutingIfNecessary(this, this._refProvider.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._refProvider.value?.updateComplete;
|
||||||
this._refProvider.value?.pause();
|
this._refProvider.value?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._refProvider.value?.updateComplete;
|
||||||
this._refProvider.value?.mute();
|
this._refProvider.value?.mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._refProvider.value?.updateComplete;
|
||||||
this._refProvider.value?.unmute();
|
this._refProvider.value?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,7 +766,9 @@ export class FrigateCardLiveProvider
|
|||||||
return this._refProvider.value?.isMuted() ?? true;
|
return this._refProvider.value?.isMuted() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._refProvider.value?.updateComplete;
|
||||||
this._refProvider.value?.seek(seconds);
|
this._refProvider.value?.seek(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,25 +837,34 @@ export class FrigateCardLiveProvider
|
|||||||
if (changedProps.has('liveConfig')) {
|
if (changedProps.has('liveConfig')) {
|
||||||
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
|
||||||
if (this.liveConfig?.show_image_during_load) {
|
if (this.liveConfig?.show_image_during_load) {
|
||||||
import('./live-image.js');
|
this._importPromises.push(import('./live-image.js'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changedProps.has('cameraConfig')) {
|
if (changedProps.has('cameraConfig')) {
|
||||||
const provider = this._getResolvedProvider();
|
const provider = this._getResolvedProvider();
|
||||||
if (provider === 'jsmpeg') {
|
if (provider === 'jsmpeg') {
|
||||||
import('./live-jsmpeg.js');
|
this._importPromises.push(import('./live-jsmpeg.js'));
|
||||||
} else if (provider === 'ha') {
|
} else if (provider === 'ha') {
|
||||||
import('./live-ha.js');
|
this._importPromises.push(import('./live-ha.js'));
|
||||||
} else if (provider === 'webrtc-card') {
|
} else if (provider === 'webrtc-card') {
|
||||||
import('./live-webrtc-card.js');
|
this._importPromises.push(import('./live-webrtc-card.js'));
|
||||||
} else if (provider === 'image') {
|
} else if (provider === 'image') {
|
||||||
import('./live-image.js');
|
this._importPromises.push(import('./live-image.js'));
|
||||||
} else if (provider === 'go2rtc') {
|
} else if (provider === 'go2rtc') {
|
||||||
import('./live-go2rtc.js');
|
this._importPromises.push(import('./live-go2rtc.js'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override async getUpdateComplete(): Promise<boolean> {
|
||||||
|
// See 'A note on dynamic imports' above for explanation of why this is
|
||||||
|
// necessary.
|
||||||
|
const result = await super.getUpdateComplete();
|
||||||
|
await Promise.all(this._importPromises);
|
||||||
|
this._importPromises = [];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
|
|||||||
@@ -49,9 +49,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public timelineConfig?: MiniTimelineControlConfig;
|
public timelineConfig?: MiniTimelineControlConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public inBackground?: boolean;
|
|
||||||
|
|
||||||
// If fetchMedia is not specified, no fetching is done.
|
// If fetchMedia is not specified, no fetching is done.
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
||||||
@@ -75,7 +72,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
!this.cardWideConfig ||
|
!this.cardWideConfig ||
|
||||||
!this.fetchMedia ||
|
!this.fetchMedia ||
|
||||||
this.inBackground ||
|
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.view ||
|
!this.view ||
|
||||||
this.view.query ||
|
this.view.query ||
|
||||||
@@ -131,9 +127,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
// do so if properties relevant to the request have changed (as per their
|
// do so if properties relevant to the request have changed (as per their
|
||||||
// hasChanged).
|
// hasChanged).
|
||||||
if (
|
if (
|
||||||
['view', 'fetch', 'browseMediaParams', 'inBackground'].some((prop) =>
|
['view', 'fetch', 'browseMediaParams'].some((prop) => changedProperties.has(prop))
|
||||||
changedProperties.has(prop),
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
this._fetchMedia();
|
this._fetchMedia();
|
||||||
}
|
}
|
||||||
@@ -162,7 +156,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.view || !this.thumbnailConfig) {
|
if (!this.hass || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,9 +177,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
@frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')}
|
@frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')}
|
||||||
@frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
@frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||||
>
|
>
|
||||||
${this.thumbnailConfig &&
|
${this.thumbnailConfig && this.thumbnailConfig.mode !== 'none'
|
||||||
this.thumbnailConfig.mode !== 'none' &&
|
|
||||||
!this.inBackground
|
|
||||||
? html` <frigate-card-thumbnail-carousel
|
? html` <frigate-card-thumbnail-carousel
|
||||||
slot=${this.thumbnailConfig.mode}
|
slot=${this.thumbnailConfig.mode}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -215,15 +207,14 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
>
|
>
|
||||||
</frigate-card-thumbnail-carousel>`
|
</frigate-card-thumbnail-carousel>`
|
||||||
: ''}
|
: ''}
|
||||||
${this.timelineConfig?.mode &&
|
${this.timelineConfig && this.timelineConfig.mode !== 'none'
|
||||||
this.timelineConfig.mode !== 'none' &&
|
|
||||||
!this.inBackground
|
|
||||||
? html` <frigate-card-timeline-core
|
? html` <frigate-card-timeline-core
|
||||||
slot=${this.timelineConfig.mode}
|
slot=${this.timelineConfig.mode}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.itemClickAction=${this.view.isViewerView() ||
|
.itemClickAction=${this.view.isViewerView() ||
|
||||||
this.thumbnailConfig.mode === 'none'
|
!this.thumbnailConfig ||
|
||||||
|
this.thumbnailConfig?.mode === 'none'
|
||||||
? 'play'
|
? 'play'
|
||||||
: 'select'}
|
: 'select'}
|
||||||
.cameraIDs=${this._cameraIDsForTimeline}
|
.cameraIDs=${this._cameraIDsForTimeline}
|
||||||
|
|||||||
@@ -76,9 +76,11 @@ interface TimelineRangeChange extends TimelineWindow {
|
|||||||
|
|
||||||
interface TimelineViewContext {
|
interface TimelineViewContext {
|
||||||
window?: TimelineWindow;
|
window?: TimelineWindow;
|
||||||
|
panBehavior?: TimelinePanBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimelineItemClickAction = 'play' | 'select';
|
type TimelineItemClickAction = 'play' | 'select';
|
||||||
|
type TimelinePanBehavior = 'pan' | 'seek' | 'seek-in-media';
|
||||||
|
|
||||||
declare module 'view' {
|
declare module 'view' {
|
||||||
interface ViewContext {
|
interface ViewContext {
|
||||||
@@ -197,7 +199,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
public itemClickAction?: TimelineItemClickAction;
|
public itemClickAction?: TimelineItemClickAction;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _locked = false;
|
protected _panBehavior: TimelinePanBehavior = 'seek';
|
||||||
|
|
||||||
protected _targetBarVisible = false;
|
protected _targetBarVisible = false;
|
||||||
|
|
||||||
@@ -272,9 +274,18 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
|
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
|
||||||
const lockTitle = this._locked
|
const panTitle =
|
||||||
? localize('timeline.unlock')
|
this._panBehavior === 'pan'
|
||||||
: localize('timeline.lock');
|
? localize('timeline.pan_behavior.pan')
|
||||||
|
: this._panBehavior === 'seek'
|
||||||
|
? localize('timeline.pan_behavior.seek')
|
||||||
|
: localize('timeline.pan_behavior.seek-in-media');
|
||||||
|
const panIcon =
|
||||||
|
this._panBehavior === 'pan'
|
||||||
|
? 'mdi:pan-horizontal'
|
||||||
|
: this._panBehavior === 'seek'
|
||||||
|
? 'mdi:filmstrip'
|
||||||
|
: 'mdi:lock';
|
||||||
|
|
||||||
return html` ${capabilities?.supportsTimeline
|
return html` ${capabilities?.supportsTimeline
|
||||||
? html` <div
|
? html` <div
|
||||||
@@ -287,12 +298,17 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
<div class="timeline-tools">
|
<div class="timeline-tools">
|
||||||
${this._shouldSupportSeeking()
|
${this._shouldSupportSeeking()
|
||||||
? html` <ha-icon
|
? html` <ha-icon
|
||||||
.icon=${`mdi:${this._locked ? 'lock' : 'lock-open-variant'}`}
|
.icon=${panIcon}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this._locked = !this._locked;
|
this._panBehavior =
|
||||||
|
this._panBehavior === 'pan'
|
||||||
|
? 'seek'
|
||||||
|
: this._panBehavior === 'seek'
|
||||||
|
? 'seek-in-media'
|
||||||
|
: 'pan';
|
||||||
}}
|
}}
|
||||||
aria-label="${lockTitle}"
|
aria-label="${panTitle}"
|
||||||
title="${lockTitle}"
|
title="${panTitle}"
|
||||||
>
|
>
|
||||||
</ha-icon>`
|
</ha-icon>`
|
||||||
: ''}
|
: ''}
|
||||||
@@ -340,9 +356,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
this._shouldSupportSeeking() &&
|
||||||
this._timeline &&
|
this._timeline &&
|
||||||
properties.byUser &&
|
properties.byUser &&
|
||||||
// Do not adjust select children or seek during zoom events.
|
// Do not adjust select/seek media during zoom events.
|
||||||
properties.event.type !== 'wheel' &&
|
properties.event.type !== 'wheel' &&
|
||||||
properties.event.additionalEvent !== 'pinchin' &&
|
properties.event.additionalEvent !== 'pinchin' &&
|
||||||
properties.event.additionalEvent !== 'pinchout'
|
properties.event.additionalEvent !== 'pinchout'
|
||||||
@@ -365,13 +382,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _shouldSupportSeeking(): boolean {
|
protected _shouldSupportSeeking(): boolean {
|
||||||
const cameraIDs = this._getTimelineCameraIDs();
|
return this.mini;
|
||||||
if (!this._timeline || !cameraIDs) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
|
|
||||||
return (this.view?.isViewerView() && capabilities?.canSeek) ?? false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,8 +396,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
|
|
||||||
const targetBarOn =
|
const targetBarOn =
|
||||||
this._shouldSupportSeeking() &&
|
this._shouldSupportSeeking() &&
|
||||||
(!this._locked ||
|
(this._panBehavior === 'seek' ||
|
||||||
(this.mini &&
|
(this._panBehavior === 'seek-in-media' &&
|
||||||
this._timeline.getSelection().some((id) => {
|
this._timeline.getSelection().some((id) => {
|
||||||
const item = this._timelineSource?.dataset?.get(id);
|
const item = this._timelineSource?.dataset?.get(id);
|
||||||
return (
|
return (
|
||||||
@@ -439,6 +450,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
!this.view ||
|
!this.view ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
|
this._panBehavior === 'pan' ||
|
||||||
// Skip range changes that do not have hammerjs pan directions associated
|
// Skip range changes that do not have hammerjs pan directions associated
|
||||||
// with them, as these outliers cause media matching issues below.
|
// with them, as these outliers cause media matching issues below.
|
||||||
!properties.event.additionalEvent
|
!properties.event.additionalEvent
|
||||||
@@ -446,37 +458,38 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canSeek = !!this.view?.isViewerView();
|
const canSeek = this._shouldSupportSeeking();
|
||||||
const newResults = this._locked
|
const newResults =
|
||||||
? null
|
this._panBehavior === 'seek-in-media'
|
||||||
: results
|
? null
|
||||||
.clone()
|
: results
|
||||||
.resetSelectedResult()
|
.clone()
|
||||||
.selectBestResult((media) =>
|
.resetSelectedResult()
|
||||||
findClosestMediaIndex(
|
.selectBestResult((media) =>
|
||||||
media,
|
findClosestMediaIndex(
|
||||||
targetTime,
|
media,
|
||||||
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
targetTime,
|
||||||
),
|
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
const desiredView: FrigateCardView = this.mini
|
||||||
canSeek ||
|
? targetTime >= new Date()
|
||||||
(newResults &&
|
? 'live'
|
||||||
newResults.hasSelectedResult() &&
|
: 'media'
|
||||||
newResults.getResult() !== results.getResult())
|
: this.view.view;
|
||||||
) {
|
|
||||||
this.view
|
this.view
|
||||||
.evolve({
|
.evolve({
|
||||||
...(newResults &&
|
view: desiredView,
|
||||||
newResults.hasSelectedResult() && { queryResults: newResults }),
|
...(newResults &&
|
||||||
}) // Whether or not to set the timeline window.
|
newResults.hasSelectedResult() && { queryResults: newResults }),
|
||||||
.mergeInContext({
|
}) // Whether or not to set the timeline window.
|
||||||
...(canSeek && { mediaViewer: { seek: targetTime } }),
|
.mergeInContext({
|
||||||
...this._setWindowInContext(properties),
|
...(canSeek && { mediaViewer: { seek: targetTime } }),
|
||||||
})
|
...this._getTimelineContext(properties),
|
||||||
.dispatchChangeEvent(this);
|
})
|
||||||
}
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -667,7 +680,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
// 'flicker' for the user as the viewer reloads all the media).
|
// 'flicker' for the user as the viewer reloads all the media).
|
||||||
const newResults = newView?.queryResults;
|
const newResults = newView?.queryResults;
|
||||||
if (newView && newResults && !this.view.queryResults?.isSupersetOf(newResults)) {
|
if (newView && newResults && !this.view.queryResults?.isSupersetOf(newResults)) {
|
||||||
newView?.mergeInContext(this._setWindowInContext())?.dispatchChangeEvent(this);
|
newView?.mergeInContext(this._getTimelineContext())?.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -948,6 +961,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
: null;
|
: null;
|
||||||
const context = this.view.context?.timeline;
|
const context = this.view.context?.timeline;
|
||||||
|
|
||||||
|
if (context && context.panBehavior) {
|
||||||
|
this._panBehavior = context.panBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
if (context && context.window) {
|
if (context && context.window) {
|
||||||
desiredWindow = context.window;
|
desiredWindow = context.window;
|
||||||
} else if (media && mediaWindow && !rangesOverlap(mediaWindow, timelineWindow)) {
|
} else if (media && mediaWindow && !rangesOverlap(mediaWindow, timelineWindow)) {
|
||||||
@@ -1021,7 +1038,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
!this._alreadyHasAcceptableMediaQuery(freshMediaQuery)
|
!this._alreadyHasAcceptableMediaQuery(freshMediaQuery)
|
||||||
) {
|
) {
|
||||||
(await this._createViewWithEventMediaQuery(freshMediaQuery))
|
(await this._createViewWithEventMediaQuery(freshMediaQuery))
|
||||||
?.mergeInContext(this._setWindowInContext(desiredWindow))
|
?.mergeInContext(this._getTimelineContext(desiredWindow))
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1046,11 +1063,12 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* Generate the context for timeline views.
|
* Generate the context for timeline views.
|
||||||
* @returns The TimelineViewContext object.
|
* @returns The TimelineViewContext object.
|
||||||
*/
|
*/
|
||||||
protected _setWindowInContext(window?: TimelineWindow): ViewContext {
|
protected _getTimelineContext(window?: TimelineWindow): ViewContext {
|
||||||
const newWindow = window ?? this._timeline?.getWindow();
|
const newWindow = window ?? this._timeline?.getWindow();
|
||||||
return {
|
return {
|
||||||
timeline: {
|
timeline: {
|
||||||
...this.view?.context?.timeline,
|
...this.view?.context?.timeline,
|
||||||
|
panBehavior: this._panBehavior,
|
||||||
...(newWindow && { window: newWindow }),
|
...(newWindow && { window: newWindow }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ import { View } from '../view/view';
|
|||||||
import './surround.js';
|
import './surround.js';
|
||||||
import './timeline-core.js';
|
import './timeline-core.js';
|
||||||
|
|
||||||
// This file is kept separate from timeline-core.ts to avoid a circular dependency:
|
|
||||||
// FrigateCardTimeline ->
|
|
||||||
// FrigateCardSurround ->
|
|
||||||
// FrigateCardTimelineCore
|
|
||||||
|
|
||||||
@customElement('frigate-card-timeline')
|
@customElement('frigate-card-timeline')
|
||||||
export class FrigateCardTimeline extends LitElement {
|
export class FrigateCardTimeline extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -38,12 +33,7 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <frigate-card-surround
|
return html`
|
||||||
.hass=${this.hass}
|
|
||||||
.view=${this.view}
|
|
||||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
|
||||||
.cameraManager=${this.cameraManager}
|
|
||||||
>
|
|
||||||
<frigate-card-timeline-core
|
<frigate-card-timeline-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
@@ -56,7 +46,7 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
: 'select'}
|
: 'select'}
|
||||||
>
|
>
|
||||||
</frigate-card-timeline-core>
|
</frigate-card-timeline-core>
|
||||||
</frigate-card-surround>`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -142,14 +142,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
return renderProgressIndicator({ cardWideConfig: this.cardWideConfig });
|
||||||
}
|
}
|
||||||
|
|
||||||
return html` <frigate-card-surround
|
return html`
|
||||||
.hass=${this.hass}
|
|
||||||
.view=${this.view}
|
|
||||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
|
||||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
|
||||||
.cameraManager=${this.cameraManager}
|
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
|
||||||
>
|
|
||||||
<frigate-card-viewer-carousel
|
<frigate-card-viewer-carousel
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
@@ -159,7 +152,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer-carousel>
|
</frigate-card-viewer-carousel>
|
||||||
</frigate-card-surround>`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -565,17 +558,17 @@ export class FrigateCardViewerProvider
|
|||||||
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async play(): Promise<void> {
|
||||||
playMediaMutingIfNecessary(
|
await playMediaMutingIfNecessary(
|
||||||
this,
|
this,
|
||||||
this._refFrigateCardMediaPlayer.value ?? this._refVideoProvider.value,
|
this._refFrigateCardMediaPlayer.value ?? this._refVideoProvider.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
(this._refFrigateCardMediaPlayer.value || this._refVideoProvider.value)?.pause();
|
(this._refFrigateCardMediaPlayer.value || this._refVideoProvider.value)?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
if (this._refFrigateCardMediaPlayer.value) {
|
if (this._refFrigateCardMediaPlayer.value) {
|
||||||
this._refFrigateCardMediaPlayer.value?.mute();
|
this._refFrigateCardMediaPlayer.value?.mute();
|
||||||
} else if (this._refVideoProvider.value) {
|
} else if (this._refVideoProvider.value) {
|
||||||
@@ -583,7 +576,7 @@ export class FrigateCardViewerProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
if (this._refFrigateCardMediaPlayer.value) {
|
if (this._refFrigateCardMediaPlayer.value) {
|
||||||
this._refFrigateCardMediaPlayer.value?.mute();
|
this._refFrigateCardMediaPlayer.value?.mute();
|
||||||
} else if (this._refVideoProvider.value) {
|
} else if (this._refVideoProvider.value) {
|
||||||
@@ -600,7 +593,7 @@ export class FrigateCardViewerProvider
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
if (this._refFrigateCardMediaPlayer.value) {
|
if (this._refFrigateCardMediaPlayer.value) {
|
||||||
return this._refFrigateCardMediaPlayer.value.seek(seconds);
|
return this._refFrigateCardMediaPlayer.value.seek(seconds);
|
||||||
} else if (this._refVideoProvider.value) {
|
} else if (this._refVideoProvider.value) {
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
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 { CameraManager } from '../camera-manager/manager.js';
|
||||||
|
import { ConditionState, getOverridesByKey } from '../card-condition';
|
||||||
|
import viewsStyle from '../scss/views.scss';
|
||||||
|
import { CardWideConfig, ExtendedHomeAssistant, FrigateCardConfig } from '../types.js';
|
||||||
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
|
import { View } from '../view/view.js';
|
||||||
|
import './surround.js';
|
||||||
|
|
||||||
|
@customElement('frigate-card-views')
|
||||||
|
export class FrigateCardViews extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public view?: Readonly<View>;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public config?: FrigateCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public nonOverriddenConfig?: FrigateCardConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public conditionState?: ConditionState;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public cameras?: ConditionState;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hide?: boolean;
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
|
if (changedProps.has('view') || changedProps.has('config')) {
|
||||||
|
if (this.view?.is('live') || this._shouldLivePreload()) {
|
||||||
|
import('./live/live.js');
|
||||||
|
}
|
||||||
|
if (this.view?.isGalleryView()) {
|
||||||
|
import('./gallery.js');
|
||||||
|
} else if (this.view?.isViewerView()) {
|
||||||
|
import('./viewer.js');
|
||||||
|
} else if (this.view?.is('image')) {
|
||||||
|
import('./image.js');
|
||||||
|
} else if (this.view?.is('timeline')) {
|
||||||
|
import('./timeline.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('hide')) {
|
||||||
|
if (this.hide) {
|
||||||
|
this.setAttribute('hidden', '');
|
||||||
|
} else {
|
||||||
|
this.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected shouldUpdate(_: PropertyValues): boolean {
|
||||||
|
// Future: Updates to `hass` and `conditionState` here will be frequent.
|
||||||
|
// Throttling here may be necessary if users report performance degradation
|
||||||
|
// > v5.0.0-beta1 .
|
||||||
|
//
|
||||||
|
// These updates are necessary in these cases:
|
||||||
|
// - conditionState: Required to let `frigate-card-live` calculate its own
|
||||||
|
// overrides.
|
||||||
|
// - hass: Required for anything that needs to sign URLs. Of note is
|
||||||
|
// anything that renders an image (e.g. a thumbnail -- almost everything,
|
||||||
|
// or the main `frigate-card-image` view).
|
||||||
|
//
|
||||||
|
// It should instead be possible to pass conditionState to live only (every
|
||||||
|
// update required), and pass hass only once / 5 minutes (see
|
||||||
|
// HASS_REJECTION_CUTOFF_MS).
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _shouldLivePreload(): boolean {
|
||||||
|
return !!this.config?.live.preload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
// 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 || !this.config || !this.nonOverriddenConfig) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render but hide the live view if there's a message, or if it's preload
|
||||||
|
// mode and the view is not live.
|
||||||
|
const liveClasses = {
|
||||||
|
hidden: this._shouldLivePreload() && !this.view?.is('live'),
|
||||||
|
};
|
||||||
|
const overallClasses = {
|
||||||
|
hidden: !!this.hide,
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbnailConfig = this.view?.is('live')
|
||||||
|
? this.config.live.controls.thumbnails
|
||||||
|
: this.view?.isViewerView()
|
||||||
|
? this.config.media_viewer.controls.thumbnails
|
||||||
|
: this.view?.is('timeline')
|
||||||
|
? this.config.timeline.controls.thumbnails
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const miniTimelineConfig = this.view?.is('live')
|
||||||
|
? this.config.live.controls.timeline
|
||||||
|
: this.view?.isViewerView()
|
||||||
|
? this.config.media_viewer.controls.timeline
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const cameraConfig = this.view
|
||||||
|
? this.cameraManager?.getStore().getCameraConfig(this.view.camera) ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return html` <frigate-card-surround
|
||||||
|
class="${classMap(overallClasses)}"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.fetchMedia=${this.view?.is('live')
|
||||||
|
? this.config.live.controls.thumbnails.media
|
||||||
|
: undefined}
|
||||||
|
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||||
|
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
>
|
||||||
|
${!this.hide && this.view?.is('image') && cameraConfig
|
||||||
|
? html` <frigate-card-image
|
||||||
|
.imageConfig=${this.config.image}
|
||||||
|
.view=${this.view}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cameraConfig=${cameraConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-image>`
|
||||||
|
: ``}
|
||||||
|
${!this.hide && this.view?.isGalleryView()
|
||||||
|
? html` <frigate-card-gallery
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.galleryConfig=${this.config.media_gallery}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-gallery>`
|
||||||
|
: ``}
|
||||||
|
${!this.hide && this.view?.isViewerView()
|
||||||
|
? html`
|
||||||
|
<frigate-card-viewer
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.viewerConfig=${this.config.media_viewer}
|
||||||
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-viewer>
|
||||||
|
`
|
||||||
|
: ``}
|
||||||
|
${!this.hide && this.view?.is('timeline')
|
||||||
|
? html` <frigate-card-timeline
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.timelineConfig=${this.config.timeline}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-timeline>`
|
||||||
|
: ``}
|
||||||
|
${
|
||||||
|
// Note: Subtle difference in condition below vs the other views in order
|
||||||
|
// to always render the live view for live.preload mode.
|
||||||
|
|
||||||
|
// Note: <frigate-card-live> uses nonOverriddenConfig rather than the
|
||||||
|
// overriden config as it does it's own overriding as part of the camera
|
||||||
|
// carousel.
|
||||||
|
this._shouldLivePreload() || (!this.hide && this.view?.is('live'))
|
||||||
|
? html`
|
||||||
|
<frigate-card-live
|
||||||
|
.hass=${this.hass}
|
||||||
|
.view=${this.view}
|
||||||
|
.liveConfig=${this.nonOverriddenConfig.live}
|
||||||
|
.conditionState=${this.conditionState}
|
||||||
|
.liveOverrides=${getOverridesByKey(this.config.overrides, 'live')}
|
||||||
|
.cameraManager=${this.cameraManager}
|
||||||
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
|
class="${classMap(liveClasses)}"
|
||||||
|
>
|
||||||
|
</frigate-card-live>
|
||||||
|
`
|
||||||
|
: ``
|
||||||
|
}
|
||||||
|
</frigate-card-surround>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(viewsStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-views': FrigateCardViews;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -460,9 +460,12 @@
|
|||||||
"timeline": "See media in timeline"
|
"timeline": "See media in timeline"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"lock": "Lock timeline to a single event",
|
"pan_behavior": {
|
||||||
"select_date": "Choose date",
|
"pan": "Pan",
|
||||||
"unlock": "Unlock timeline"
|
"seek": "Pan seeks across all media",
|
||||||
|
"seek-in-media": "Pan seeks within selected media item only"
|
||||||
|
},
|
||||||
|
"select_date": "Choose date"
|
||||||
},
|
},
|
||||||
"elements": {
|
"elements": {
|
||||||
"ptz": {
|
"ptz": {
|
||||||
|
|||||||
@@ -450,9 +450,12 @@
|
|||||||
"timeline": "Vedi evento nella timeline"
|
"timeline": "Vedi evento nella timeline"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"lock": "Blocca la sequenza temporale su un singolo evento",
|
"pan_behavior": {
|
||||||
"select_date": "Scegli la data",
|
"pan": "",
|
||||||
"unlock": "Sblocca la cronologia"
|
"seek": "",
|
||||||
|
"seek-in-media": ""
|
||||||
|
},
|
||||||
|
"select_date": "Scegli la data"
|
||||||
},
|
},
|
||||||
"elements": {
|
"elements": {
|
||||||
"ptz": {
|
"ptz": {
|
||||||
|
|||||||
@@ -471,8 +471,11 @@
|
|||||||
"timeline": "Ver evento na linha do tempo"
|
"timeline": "Ver evento na linha do tempo"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"lock": "Bloquear linha do tempo em um único evento",
|
"pan_behavior": {
|
||||||
"select_date": "Escolha a data",
|
"pan": "",
|
||||||
"unlock": "Desbloquear linha do tempo"
|
"seek": "",
|
||||||
|
"seek-in-media": ""
|
||||||
|
},
|
||||||
|
"select_date": "Escolha a data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,15 +51,15 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
return this._player?.play();
|
return this._player?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._player?.pause();
|
this._player?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
this._player?.mute();
|
this._player?.mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
this._player?.unmute();
|
this._player?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +67,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
return this._player?.isMuted() ?? true;
|
return this._player?.isMuted() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async seek(seconds: number): Promise<void> {
|
||||||
* Seek the video (unsupported).
|
|
||||||
*/
|
|
||||||
public seek(seconds: number): void {
|
|
||||||
this._player?.seek(seconds);
|
this._player?.seek(seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
return this._video?.play();
|
return this._video?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._video?.pause();
|
this._video?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
// The muted property is only for the initial muted state. Must explicitly
|
// The muted property is only for the initial muted state. Must explicitly
|
||||||
// set the muted on the video player to make the change dynamic.
|
// set the muted on the video player to make the change dynamic.
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
@@ -49,7 +49,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
// See note in mute().
|
// See note in mute().
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
this._video.muted = false;
|
this._video.muted = false;
|
||||||
@@ -60,7 +60,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
return this._video?.muted ?? true;
|
return this._video?.muted ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
hideMediaControlsTemporarily(this._video);
|
hideMediaControlsTemporarily(this._video);
|
||||||
this._video.currentTime = seconds;
|
this._video.currentTime = seconds;
|
||||||
|
|||||||
@@ -9,16 +9,17 @@
|
|||||||
// available as compilation time.
|
// available as compilation time.
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
|
import { css, CSSResultGroup, html, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { dispatchErrorMessageEvent } from '../components/message.js';
|
||||||
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
||||||
import {
|
import {
|
||||||
hideMediaControlsTemporarily,
|
hideMediaControlsTemporarily,
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS
|
||||||
} from '../utils/media.js';
|
} from '../utils/media.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
|
||||||
|
|
||||||
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||||
@customElement('frigate-card-ha-web-rtc-player')
|
@customElement('frigate-card-ha-web-rtc-player')
|
||||||
@@ -36,11 +37,11 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
return this._video?.play();
|
return this._video?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(): void {
|
public async pause(): Promise<void> {
|
||||||
this._video?.pause();
|
this._video?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public async mute(): Promise<void> {
|
||||||
// The muted property is only for the initial muted state. Must explicitly
|
// The muted property is only for the initial muted state. Must explicitly
|
||||||
// set the muted on the video player to make the change dynamic.
|
// set the muted on the video player to make the change dynamic.
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
@@ -48,7 +49,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
// See note in mute().
|
// See note in mute().
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
this._video.muted = false;
|
this._video.muted = false;
|
||||||
@@ -59,7 +60,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
return this._video?.muted ?? true;
|
return this._video?.muted ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public seek(seconds: number): void {
|
public async seek(seconds: number): Promise<void> {
|
||||||
if (this._video) {
|
if (this._video) {
|
||||||
this._video.currentTime = seconds;
|
this._video.currentTime = seconds;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-7
@@ -37,8 +37,7 @@ div.main {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: block;
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
// Necessary to get Safari to show border-radius correctly.
|
// Necessary to get Safari to show border-radius correctly.
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
@@ -118,11 +117,6 @@ ha-card.triggered {
|
|||||||
animation: warning-pulse 5s infinite;
|
animation: warning-pulse 5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
frigate-card-live.hidden {
|
|
||||||
// Live view will be rendered but hidden for live preloading.
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/************
|
/************
|
||||||
* Fullscreen
|
* Fullscreen
|
||||||
*************/
|
*************/
|
||||||
|
|||||||
+1
-8
@@ -1,12 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
frigate-card-live-carousel {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
::slotted:not([name]) {
|
::slotted(:not([slot])) {
|
||||||
// Expand the main body to fill available content not otherwise used by the
|
// Expand the main body to fill available content not otherwise used by the
|
||||||
// surround.
|
// named slots around the surround.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
:host {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([hidden]),
|
||||||
|
.hidden {
|
||||||
|
// Views content is hidden in these cases:
|
||||||
|
// - Live view being preloaded but live view not active.
|
||||||
|
// - Overall views component marked as hidden (e.g. error message to display
|
||||||
|
// at the card level).
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
+4
-4
@@ -1440,11 +1440,11 @@ export interface StateParameters {
|
|||||||
|
|
||||||
export interface FrigateCardMediaPlayer {
|
export interface FrigateCardMediaPlayer {
|
||||||
play(): Promise<void>;
|
play(): Promise<void>;
|
||||||
pause(): void;
|
pause(): Promise<void>;
|
||||||
mute(): void;
|
mute(): Promise<void>;
|
||||||
unmute(): void;
|
unmute(): Promise<void>;
|
||||||
isMuted(): boolean;
|
isMuted(): boolean;
|
||||||
seek(seconds: number): void;
|
seek(seconds: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardHelpers {
|
export interface CardHelpers {
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export function createMediaLoadedInfo(
|
|||||||
* @param source An event or HTMLElement that should be used as a source.
|
* @param source An event or HTMLElement that should be used as a source.
|
||||||
*/
|
*/
|
||||||
export function dispatchMediaLoadedEvent(
|
export function dispatchMediaLoadedEvent(
|
||||||
element: HTMLElement,
|
target: HTMLElement,
|
||||||
source: Event | HTMLElement,
|
source: Event | HTMLElement,
|
||||||
): void {
|
): void {
|
||||||
const mediaLoadedInfo = createMediaLoadedInfo(source);
|
const mediaLoadedInfo = createMediaLoadedInfo(source);
|
||||||
if (mediaLoadedInfo) {
|
if (mediaLoadedInfo) {
|
||||||
dispatchExistingMediaLoadedInfoAsEvent(element, mediaLoadedInfo);
|
dispatchExistingMediaLoadedInfoAsEvent(target, mediaLoadedInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +59,10 @@ export function dispatchMediaLoadedEvent(
|
|||||||
* @param MediaLoadedInfo The MediaLoadedInfo object to send.
|
* @param MediaLoadedInfo The MediaLoadedInfo object to send.
|
||||||
*/
|
*/
|
||||||
export function dispatchExistingMediaLoadedInfoAsEvent(
|
export function dispatchExistingMediaLoadedInfoAsEvent(
|
||||||
element: HTMLElement,
|
target: EventTarget,
|
||||||
MediaLoadedInfo: MediaLoadedInfo,
|
MediaLoadedInfo: MediaLoadedInfo,
|
||||||
): void {
|
): void {
|
||||||
dispatchFrigateCardEvent<MediaLoadedInfo>(element, 'media:loaded', MediaLoadedInfo);
|
dispatchFrigateCardEvent<MediaLoadedInfo>(target, 'media:loaded', MediaLoadedInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export const executeMediaQueryForView = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the closest matching media object.
|
* Find the closest matching media object.
|
||||||
* @param mediaArray The media. Must be sorted most recent first.
|
* @param mediaArray The media.
|
||||||
* @param targetTime The target time used to find the relevant child.
|
* @param targetTime The target time used to find the relevant child.
|
||||||
* @param refPoint Whether to find based on the start or end of the
|
* @param refPoint Whether to find based on the start or end of the
|
||||||
* event/recording. If not specified, the first match is returned rather than
|
* event/recording. If not specified, the first match is returned rather than
|
||||||
|
|||||||
+7
-4
@@ -31,7 +31,6 @@ export const hideMediaControlsTemporarily = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param player The Frigate Card Media Player object.
|
* @param player The Frigate Card Media Player object.
|
||||||
* @param video An underlying video or media player upon which to call play.
|
* @param video An underlying video or media player upon which to call play.
|
||||||
*/
|
*/
|
||||||
@@ -43,10 +42,14 @@ export const playMediaMutingIfNecessary = async (
|
|||||||
// and then try again. This works around some browsers that prevent
|
// and then try again. This works around some browsers that prevent
|
||||||
// auto-play unless the video is muted.
|
// auto-play unless the video is muted.
|
||||||
if (video?.play) {
|
if (video?.play) {
|
||||||
video.play().catch((ev) => {
|
video.play().catch(async (ev) => {
|
||||||
if (ev.name === 'NotAllowedError' && !player.isMuted()) {
|
if (ev.name === 'NotAllowedError' && !player.isMuted()) {
|
||||||
player.mute();
|
await player.mute();
|
||||||
video.play().catch();
|
try {
|
||||||
|
await video.play();
|
||||||
|
} catch (_) {
|
||||||
|
// Pass.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ export class ViewMedia {
|
|||||||
}
|
}
|
||||||
public includesTime(seek: Date): boolean {
|
public includesTime(seek: Date): boolean {
|
||||||
const startTime = this.getStartTime();
|
const startTime = this.getStartTime();
|
||||||
const endTime = this.getEndTime();
|
const endTime = this.getEndTime() ?? startTime;
|
||||||
return !!startTime && !!endTime && seek >= startTime && seek <= endTime;
|
return !!startTime && !!endTime && seek >= startTime && seek <= endTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user