Initial motionEye commit.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> `
|
||||
|
||||
@@ -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 : '';
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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) ?? ''}"
|
||||
|
||||
Reference in New Issue
Block a user