Initial motionEye commit.

This commit is contained in:
Dermot Duffy
2023-03-26 16:42:21 -07:00
parent 05e9db4cc5
commit 304c7e3164
55 changed files with 1780 additions and 245 deletions
+10 -4
View File
@@ -23,7 +23,7 @@ import {
} from '../utils/media-to-view.js';
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
import { View } from '../view/view.js';
import { dispatchMessageEvent, renderProgressIndicator } from './message.js';
import { renderMessage, renderProgressIndicator } from './message.js';
import './thumbnail.js';
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
@@ -432,13 +432,19 @@ export class FrigateCardGalleryCore extends LitElement {
}
if ((this.view?.queryResults?.getResultsCount() ?? 0) === 0) {
return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
// Note that this is not throwing up an error message for the card to
// handle (as typical), but rather directly rendering the message into the
// gallery. This is to allow the filter to still be available when a given
// filter selection returns no media.
return renderMessage({
type: 'info',
message: localize('common.no_media'),
icon: 'mdi:multimedia',
});
}
const selected = this.view?.queryResults?.getSelectedResult();
return html`
return html` <div class="grid">
${this._showLoaderTop
? html`${renderProgressIndicator({
cardWideConfig: this.cardWideConfig,
@@ -490,7 +496,7 @@ export class FrigateCardGalleryCore extends LitElement {
componentRef: this._refLoaderBottom,
})}`
: ''}
`;
</div>`;
}
public updated(changedProps: PropertyValues): void {
+5 -3
View File
@@ -539,8 +539,9 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig}
.cameraEndpoints=${guard([this.cameraManager, cameraID], () =>
this.cameraManager?.getCameraEndpoints(cameraID),
.cameraEndpoints=${guard(
[this.cameraManager, cameraID],
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
)}
.label=${cameraMetadata?.title ?? ''}
.liveConfig=${config}
@@ -638,6 +639,7 @@ export class FrigateCardLiveCarousel extends LitElement {
.label="${cameraMetadataCurrent
? `${localize('common.live')}: ${cameraMetadataCurrent.title}`
: ''}"
.logo="${cameraMetadataCurrent?.engineLogo}"
.titlePopupConfig=${config.controls.title}
.selected=${this._getSelectedCameraIndex()}
transitionEffect=${this._getTransitionEffect()}
@@ -723,7 +725,7 @@ export class FrigateCardLiveProvider
protected _refProvider: Ref<Element & FrigateCardMediaPlayer> = createRef();
public async play(): Promise<void> {
playMediaMutingIfNecessary(this._refProvider.value);
playMediaMutingIfNecessary(this, this._refProvider.value);
}
public pause(): void {
+4
View File
@@ -115,6 +115,9 @@ export class FrigateCardMediaCarousel extends LitElement {
@property({ attribute: false })
public label?: string;
@property({ attribute: false })
public logo?: string;
@property({ attribute: false })
public titlePopupConfig?: TitleControlConfig;
@@ -420,6 +423,7 @@ export class FrigateCardMediaCarousel extends LitElement {
${ref(this._titleControlRef)}
.config=${this.titlePopupConfig}
.text="${this.label}"
.logo="${this.logo}"
.fitInto=${this as HTMLElement}
>
</frigate-card-title-control> `
+1 -1
View File
@@ -247,7 +247,7 @@ export class FrigateCardMenu extends LitElement {
</frigate-card-submenu-select>`;
}
let stateParameters: StateParameters = { ...button };
let stateParameters = { ...button } as StateParameters;
const svgPath =
stateParameters.icon === FRIGATE_BUTTON_MENU_ICON ? FRIGATE_ICON_SVG_PATH : '';
+4 -4
View File
@@ -9,7 +9,7 @@ import {
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { styleMap } from 'lit/directives/style-map.js';
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { actionHandler } from '../action-handler-directive.js';
import submenuStyle from '../scss/submenu.scss';
import {
@@ -39,7 +39,7 @@ export class FrigateCardSubmenu extends LitElement {
if (!this.hass) {
return;
}
const stateParameters = refreshDynamicStateParameters(this.hass, { ...item });
const stateParameters = refreshDynamicStateParameters(this.hass, { ...item } as StateParameters);
const getIcon = (stateParameters: StateParameters): TemplateResult => {
if (stateParameters.icon) {
return html` <ha-icon
@@ -95,7 +95,7 @@ export class FrigateCardSubmenu extends LitElement {
@click=${(ev) => stopEventFromActivatingCardWideActions(ev)}
>
<ha-icon-button
style="${styleMap(this.submenu.style || {})}"
style="${styleMap(this.submenu.style as StyleInfo || {})}"
class="button"
slot="trigger"
.label=${this.submenu.title || ''}
@@ -205,7 +205,7 @@ export class FrigateCardSubmenuSelect extends LitElement {
icon: domainIcon('select'),
// Pull out the dynamic properties (like icon, and title) from the state.
...refreshDynamicStateParameters(this.hass, this.submenuSelect),
...refreshDynamicStateParameters(this.hass, this.submenuSelect as StateParameters),
// Override it with anything explicitly set in the submenuSelect.
...this.submenuSelect,
+34 -26
View File
@@ -170,11 +170,9 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
const rawEndTime = this.media.getEndTime();
const endTime = rawStartTime
? rawEndTime
? getDurationString(rawStartTime, rawEndTime)
: localize('event.in_progress')
: null;
const duration =
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
const inProgress = this.media.inProgress() ? localize('event.in_progress') : null;
const what = prettifyTitle(this.media.getWhat()?.join(', ')) ?? null;
const where = prettifyTitle(this.media.getWhere()?.join(', ')) ?? null;
@@ -200,13 +198,18 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
></ha-icon>
<span title="${startTime}">${startTime}</span>
</div>
<div>
<ha-icon
title=${localize('event.duration')}
.icon=${'mdi:clock-outline'}
></ha-icon>
<span title="${endTime}">${endTime}</span>
</div>`
${duration || inProgress
? html` <div>
<ha-icon
title=${localize('event.duration')}
.icon=${'mdi:clock-outline'}
></ha-icon>
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
${inProgress
? html`<span title="${inProgress}">${inProgress}</span>`
: ''}
</div>`
: ''}`
: ''}
${this.cameraTitle
? html` <div>
@@ -266,11 +269,9 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
const rawEndTime = this.media.getEndTime();
const endTime = rawStartTime
? rawEndTime
? getDurationString(rawStartTime, rawEndTime)
: localize('event.in_progress')
: null;
const duration =
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
const inProgress = this.media.inProgress() ? localize('recording.in_progress') : null;
const seek = this.seek ? format(this.seek, 'HH:mm:ss') : null;
@@ -284,17 +285,24 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
<div class="details">
${startTime
? html` <div>
<ha-icon title=${localize(
'recording.start',
)} .icon=${'mdi:calendar-clock-outline'}></ha-icon>
<ha-icon
title=${localize('recording.start')}
.icon=${'mdi:calendar-clock-outline'}
></ha-icon>
<span title="${startTime}">${startTime}</span>
</div>
<div>
<ha-icon title=${localize(
'recording.duration',
)} .icon=${'mdi:clock-outline'}></ha-icon>
<span title="${endTime}">${endTime}</span>
</div>`
${duration || inProgress
? html` <div>
<ha-icon
title=${localize('recording.duration')}
.icon=${'mdi:clock-outline'}
></ha-icon>
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
${inProgress
? html`<span title="${inProgress}">${inProgress}</span>`
: ''}
</div>`
: ''}`
: ''}
${seek
? html` <div>
+12 -10
View File
@@ -525,10 +525,13 @@ export class FrigateCardTimelineCore extends LitElement {
.selectResultIfFound((media) => media.getID() === properties.item);
if (!newResults || !newResults.hasSelectedResult()) {
// This can happen if this is a recording query (with recorded hours)
// and an event is clicked on the timeline, or if the current thumbnails
// is a filtered view from the media gallery (i.e. any case where the
// thumbnails may not be match the events on the timeline).
// This can happen in a few situations:
// - If this is a recording query (with recorded hours) and an event is
// clicked on the timeline
// - If the current thumbnails/results is a filtered view from the media
// gallery (i.e. any case where the thumbnails may not be match the
// events on the timeline, e.g. in the snapshots viewer but
// mini-timeline showing all media).
const fullEventView = await this._createViewWithEventMediaQuery(
this._createEventMediaQuerys(),
{
@@ -551,10 +554,7 @@ export class FrigateCardTimelineCore extends LitElement {
}
if (view) {
view
// If the user is clicking something in the timeline, don't
// subsequently shift the window (it's pretty jarring).
.dispatchChangeEvent(this);
view.dispatchChangeEvent(this);
if (this.view?.is('timeline')) {
dispatchFrigateCardEvent(this, 'thumbnails:open');
@@ -895,8 +895,10 @@ export class FrigateCardTimelineCore extends LitElement {
const mediaStartTime = media?.getStartTime();
const mediaEndTime = media?.getEndTime();
const mediaWindow: TimelineWindow | null =
media && mediaStartTime && mediaEndTime
? { start: mediaStartTime, end: mediaEndTime }
media && mediaStartTime
// If this media has no end time, it's just a "point" in time so the
// range effectively starts/ends at the same time.
? { start: mediaStartTime, end: mediaEndTime ?? mediaStartTime }
: null;
const context = this.view.context?.timeline;
+8 -5
View File
@@ -1,7 +1,6 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { customElement, property } from 'lit/decorators.js';
import { TitleControlConfig } from '../types.js';
import titleStyle from '../scss/title-control.scss';
@@ -21,6 +20,9 @@ export class FrigateCardTitleControl extends LitElement {
@property({ attribute: false })
public fitInto?: HTMLElement;
@property({ attribute: false })
public logo?: string;
protected _toastRef: Ref<PaperToast> = createRef();
/**
@@ -44,6 +46,7 @@ export class FrigateCardTitleControl extends LitElement {
.text="${this.text}"
.fitInto=${this.fitInto}
>
${this.logo ? html`<img src=${this.logo} />` : ''}
</paper-toast>`;
}
@@ -58,7 +61,7 @@ export class FrigateCardTitleControl extends LitElement {
/**
* Show the toast.
*/
public hide(): void {
public hide(): void {
if (this._toastRef.value) {
// Set it to false first, to ensure the timer resets.
this._toastRef.value.opened = false;
@@ -85,7 +88,7 @@ export class FrigateCardTitleControl extends LitElement {
}
declare global {
interface HTMLElementTagNameMap {
"frigate-card-title-control": FrigateCardTitleControl
}
interface HTMLElementTagNameMap {
'frigate-card-title-control': FrigateCardTitleControl;
}
}
+89 -22
View File
@@ -45,7 +45,7 @@ import {
changeViewToRecentEventsForCameraAndDependents,
changeViewToRecentRecordingForCameraAndDependents,
} from '../utils/media-to-view.js';
import { ViewMedia } from '../view/media.js';
import { VideoContentType, ViewMedia } from '../view/media.js';
import { ViewMediaClassifier } from '../view/media-classifier';
import { guard } from 'lit/directives/guard.js';
import { localize } from '../localize/localize.js';
@@ -53,6 +53,10 @@ import { MediaQueriesResults } from '../view/media-queries-results.js';
import { canonicalizeHAURL } from '../utils/ha/index.js';
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import { playMediaMutingIfNecessary } from '../utils/media.js';
import {
hideMediaControlsTemporarily,
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
} from '../utils/media.js';
export interface MediaViewerViewContext {
seek?: Date;
@@ -399,7 +403,13 @@ export class FrigateCardViewerCarousel extends LitElement {
const media =
this.view?.queryResults?.getSelectedResult() ??
this.view?.queryResults?.getResult(resultCount - 1);
if (!media || !this.view || !this.view.queryResults) {
if (
!this.hass ||
!this.cameraManager ||
!media ||
!this.view ||
!this.view.queryResults
) {
return;
}
@@ -416,6 +426,11 @@ export class FrigateCardViewerCarousel extends LitElement {
}
};
const cameraMetadata = this.cameraManager.getCameraMetadata(
this.hass,
media.getCameraID(),
);
return html` <frigate-card-media-carousel
${ref(this._refMediaCarousel)}
.carouselOptions=${guard([this.viewerConfig], () => ({
@@ -426,6 +441,7 @@ export class FrigateCardViewerCarousel extends LitElement {
this._getPlugins.bind(this),
)}
.label=${media.getTitle() ?? undefined}
.logo=${cameraMetadata?.engineLogo}
.titlePopupConfig=${this.viewerConfig?.controls.title}
.selected=${this.view?.queryResults?.getSelectedIndex() ?? 0}
transitionEffect=${this._getTransitionEffect()}
@@ -544,30 +560,53 @@ export class FrigateCardViewerProvider
@property({ attribute: false })
public cardWideConfig?: CardWideConfig;
protected _refVideoProvider: Ref<Element & FrigateCardMediaPlayer> = createRef();
protected _refFrigateCardMediaPlayer: Ref<Element & FrigateCardMediaPlayer> =
createRef();
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
public async play(): Promise<void> {
playMediaMutingIfNecessary(this._refVideoProvider.value);
playMediaMutingIfNecessary(
this,
this._refFrigateCardMediaPlayer.value ?? this._refVideoProvider.value,
);
}
public pause(): void {
this._refVideoProvider.value?.pause();
(this._refFrigateCardMediaPlayer.value || this._refVideoProvider.value)?.pause();
}
public mute(): void {
this._refVideoProvider.value?.mute();
if (this._refFrigateCardMediaPlayer.value) {
this._refFrigateCardMediaPlayer.value?.mute();
} else if (this._refVideoProvider.value) {
this._refVideoProvider.value.muted = true;
}
}
public unmute(): void {
this._refVideoProvider.value?.unmute();
if (this._refFrigateCardMediaPlayer.value) {
this._refFrigateCardMediaPlayer.value?.mute();
} else if (this._refVideoProvider.value) {
this._refVideoProvider.value.muted = false;
}
}
public isMuted(): boolean {
return this._refVideoProvider.value?.isMuted() ?? true;
if (this._refFrigateCardMediaPlayer.value) {
return this._refFrigateCardMediaPlayer.value?.isMuted() ?? true;
} else if (this._refVideoProvider.value) {
return this._refVideoProvider.value.muted;
}
return true;
}
public seek(seconds: number): void {
this._refVideoProvider.value?.seek(seconds);
if (this._refFrigateCardMediaPlayer.value) {
return this._refFrigateCardMediaPlayer.value.seek(seconds);
} else if (this._refVideoProvider.value) {
hideMediaControlsTemporarily(this._refVideoProvider.value);
this._refVideoProvider.value.currentTime = seconds;
}
}
/**
@@ -666,19 +705,47 @@ export class FrigateCardViewerProvider
}
return ViewMediaClassifier.isVideo(this.media)
? html`<frigate-card-ha-hls-player
${ref(this._refVideoProvider)}
allow-exoplayer
aria-label="${this.media.getTitle() ?? ''}"
?autoplay=${false}
controls
muted
playsinline
title="${this.media.getTitle() ?? ''}"
url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
.hass=${this.hass}
>
</frigate-card-ha-hls-player>`
? this.media.getVideoContentType() === VideoContentType.HLS
? html`<frigate-card-ha-hls-player
${ref(this._refFrigateCardMediaPlayer)}
allow-exoplayer
aria-label="${this.media.getTitle() ?? ''}"
?autoplay=${false}
controls
muted
playsinline
title="${this.media.getTitle() ?? ''}"
url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
.hass=${this.hass}
>
</frigate-card-ha-hls-player>`
: html`
<video
${ref(this._refVideoProvider)}
aria-label="${this.media.getTitle() ?? ''}"
title="${this.media.getTitle() ?? ''}"
muted
controls
playsinline
?autoplay=${false}
@loadedmetadata=${(ev: Event) => {
if (ev.target) {
hideMediaControlsTemporarily(
ev.target as HTMLVideoElement,
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
);
}
}}
@loadeddata=${(ev: Event) => {
dispatchMediaLoadedEvent(this, ev);
}}
>
<source
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
type="video/mp4"
/>
</video>
`
: html`<img
aria-label="${this.media.getTitle() ?? ''}"
src="${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}"