Initial mini-timeline commit.

This commit is contained in:
Dermot Duffy
2022-09-22 17:44:43 -07:00
parent 1244cfc925
commit eb1b44eb27
19 changed files with 1461 additions and 819 deletions
+8 -1
View File
@@ -105,7 +105,14 @@ export class FrigateCardCarousel extends LitElement {
* @param index Slide number.
*/
public carouselScrollTo(index: number): void {
this._carousel?.scrollTo(index, this.transitionEffect === 'none');
const scroll = () => this._carousel?.scrollTo(index, this.transitionEffect === 'none');
if (this._carousel) {
scroll();
} else {
this.updateComplete.then(() => {
scroll();
})
}
}
/**
+10 -4
View File
@@ -68,6 +68,7 @@ import { renderTask } from '../utils/task.js';
import { classMap } from 'lit/directives/class-map.js';
import './image';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { TimelineDataManager } from '../utils/timeline-data-manager.js';
// Number of seconds a signed URL is valid for.
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -95,10 +96,13 @@ export class FrigateCardLive extends LitElement {
@property({ attribute: false, hasChanged: contentsChanged })
public liveOverrides?: LiveOverrides;
@property({ attribute: false })
public timelineDataManager?: TimelineDataManager;
// Whether or not the live view is currently in the background (i.e. preloaded
// but not visible)
@state()
protected _inBackground?: boolean = true;
protected _inBackground?: boolean = false;
// Intersection handler is used to detect when the live view flips between
// foreground and background (in preload mode).
@@ -122,7 +126,7 @@ export class FrigateCardLive extends LitElement {
* @param entries The IntersectionObserverEntry entries (should be only 1).
*/
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
this._inBackground = entries.every((entry) => !entry.isIntersecting);
this._inBackground = !entries.some((entry) => entry.isIntersecting);
if (
!this._inBackground &&
@@ -212,10 +216,12 @@ export class FrigateCardLive extends LitElement {
html`<frigate-card-surround-thumbnails
.hass=${this.hass}
.view=${this.view}
.config=${config.controls.thumbnails}
.thumbnailConfig=${config.controls.thumbnails}
.timelineConfig=${config.controls.timeline}
.browseMediaParams=${browseMediaParams ?? undefined}
.cameras=${this.cameras}
?fetch=${!this._inBackground}
.timelineDataManager=${this.timelineDataManager}
.inBackground=${this._inBackground}
@frigate-card:message=${(ev: CustomEvent<Message>) => {
this._renderKey++;
this._messageReceivedPostRender = true;
+61 -28
View File
@@ -1,3 +1,6 @@
import './surround.js';
import './timeline';
import {
CSSResultGroup,
html,
@@ -7,6 +10,7 @@ import {
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import surroundThumbnailsStyle from '../scss/surround.scss';
import {
BrowseMediaQueryParameters,
@@ -14,7 +18,7 @@ import {
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
FrigateCardError,
FrigateCardView,
MiniTimelineControlConfig,
ThumbnailsControlConfig,
} from '../types.js';
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
@@ -22,9 +26,9 @@ import {
getFirstTrueMediaChildIndex,
multipleBrowseMediaQueryMerged,
} from '../utils/ha/browse-media';
import { TimelineDataManager } from '../utils/timeline-data-manager';
import { View } from '../view.js';
import { dispatchFrigateCardErrorEvent } from './message.js';
import './surround.js';
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
interface ThumbnailViewContext {
@@ -47,13 +51,13 @@ export class FrigateCardSurround extends LitElement {
public view?: Readonly<View>;
@property({ attribute: false, hasChanged: contentsChanged })
public config?: ThumbnailsControlConfig;
public thumbnailConfig?: ThumbnailsControlConfig;
@property({ attribute: false, hasChanged: contentsChanged })
public timelineConfig?: MiniTimelineControlConfig;
@property({ attribute: false })
public targetView?: FrigateCardView;
@property({ attribute: true, type: Boolean })
public fetch?: boolean;
public inBackground?: boolean;
@property({ attribute: false, hasChanged: contentsChanged })
public browseMediaParams?: BrowseMediaQueryParameters | BrowseMediaQueryParameters[];
@@ -61,6 +65,9 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false })
public cameras?: Map<string, CameraConfig>;
@property({ attribute: false })
public timelineDataManager?: TimelineDataManager;
/**
* Fetch thumbnail media when a target is not specified in the view (e.g. for
* the live view).
@@ -69,11 +76,11 @@ export class FrigateCardSurround extends LitElement {
*/
protected async _fetchMedia(): Promise<void> {
if (
!this.fetch ||
this.inBackground ||
!this.hass ||
!this.view ||
!this.config ||
this.config.mode === 'none' ||
!this.thumbnailConfig ||
this.thumbnailConfig.mode === 'none' ||
this.view.target ||
!this.browseMediaParams ||
!(this.view.context?.thumbnails?.fetch ?? true)
@@ -89,7 +96,6 @@ export class FrigateCardSurround extends LitElement {
if (getFirstTrueMediaChildIndex(parent) !== null) {
this.view
?.evolve({
...(this.targetView && { view: this.targetView }),
target: parent,
childIndex: null,
@@ -105,7 +111,9 @@ export class FrigateCardSurround extends LitElement {
* @returns `true` if a drawer is used, `false` otherwise.
*/
protected _hasDrawer(): boolean {
return !!this.config && ['left', 'right'].includes(this.config.mode);
return (
!!this.thumbnailConfig && ['left', 'right'].includes(this.thumbnailConfig.mode)
);
}
/**
@@ -116,7 +124,7 @@ export class FrigateCardSurround extends LitElement {
// do so if properties relevant to the request have changed (as per their
// hasChanged).
if (
['view', 'targetView', 'fetch', 'browseMediaParams'].some((prop) =>
['view', 'fetch', 'browseMediaParams', 'inBackground'].some((prop) =>
changedProperties.has(prop),
)
) {
@@ -129,7 +137,7 @@ export class FrigateCardSurround extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
if (!this.hass || !this.view || !this.config) {
if (!this.hass || !this.view || !this.thumbnailConfig) {
return;
}
@@ -139,9 +147,9 @@ export class FrigateCardSurround extends LitElement {
// (if the thumbnails are in a drawer). The new event needs to be dispatched
// from the origin of the inbound event, so it can be handled by
// <frigate-card-surround> .
if (this.config && this._hasDrawer()) {
if (this.thumbnailConfig && this._hasDrawer()) {
dispatchFrigateCardEvent(ev.composedPath()[0], 'drawer:' + action, {
drawer: this.config.mode,
drawer: this.thumbnailConfig.mode,
});
}
};
@@ -150,31 +158,56 @@ export class FrigateCardSurround extends LitElement {
@frigate-card:thumbnails:open=${(ev: CustomEvent) => changeDrawer(ev, 'open')}
@frigate-card:thumbnails:close=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
>
${this.config && this.config.mode !== 'none'
${this.thumbnailConfig &&
this.thumbnailConfig.mode !== 'none' &&
!this.inBackground
? html` <frigate-card-thumbnail-carousel
slot=${this.config.mode}
slot=${this.thumbnailConfig.mode}
.hass=${this.hass}
.config=${this.config}
.config=${this.thumbnailConfig}
.view=${this.view}
.target=${this.view.target}
.selected=${this.view.childIndex}
.cameras=${this.cameras}
@frigate-card:view:change=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
@frigate-card:thumbnail-carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
@frigate-card:thumbnail-carousel:tap=${(
ev: CustomEvent<ThumbnailCarouselTap>,
) => {
const child: FrigateBrowseMediaSource | null =
ev.detail.target?.children?.[ev.detail.childIndex] ?? null;
// Send the view change from the source of the tap event, so the
// view change will be caught by the handler above (to close the drawer).
this.view
?.evolve({
view: this.targetView || 'media',
target: ev.detail.target,
childIndex: ev.detail.childIndex,
context: null,
})
.dispatchChangeEvent(ev.composedPath()[0]);
if (child) {
this.view
?.evolve({
view: this.view.is('recording') ? 'recording' : 'media',
target: ev.detail.target,
childIndex: ev.detail.childIndex,
context: null,
...(child?.frigate?.cameraID && {
camera: child?.frigate?.cameraID,
}),
})
.dispatchChangeEvent(ev.composedPath()[0]);
}
}}
>
</frigate-card-thumbnail-carousel>`
: ''}
${this.timelineConfig && !this.inBackground
? html` <frigate-card-timeline-core
slot=${this.timelineConfig.mode}
.hass=${this.hass}
.view=${this.view}
.cameras=${this.cameras}
.mini=${true}
.timelineConfig=${this.timelineConfig}
.thumbnailDetails=${this.thumbnailConfig?.show_details}
.thumbnailSize=${this.thumbnailConfig?.size}
.timelineDataManager=${this.timelineDataManager}
>
</frigate-card-timeline-core>`
: ''}
<slot></slot>
</frigate-card-surround>`;
}
+1
View File
@@ -209,6 +209,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
.view=${this.view}
.target=${parent}
.childIndex=${childIndex}
.mediaSeek=${this.view?.context?.mediaViewer?.seek.get(childIndex)}
.clientID=${cameraConfig?.frigate.client_id}
?details=${this.config?.show_details}
?show_favorite_control=${this.config?.show_favorite_control}
+28 -11
View File
@@ -2,17 +2,12 @@ import { format, fromUnixTime } from 'date-fns';
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { localize } from '../localize/localize.js';
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
import thumbnailFeatureEventStyle from '../scss/thumbnail-feature-event.scss';
import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording.scss';
import thumbnailStyle from '../scss/thumbnail.scss';
import type {
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
FrigateEvent,
FrigateRecording,
} from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import { errorToConsole, prettifyTitle } from '../utils/basic.js';
import { retainEvent } from '../utils/frigate.js';
@@ -20,7 +15,14 @@ import { getEventDurationString } from '../utils/ha/browse-media.js';
import { renderTask } from '../utils/task.js';
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
import { View } from '../view.js';
import { MediaSeek } from './viewer.js';
import type {
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
FrigateEvent,
FrigateRecording,
} from '../types.js';
// The minimum width of a thumbnail with details enabled.
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
@@ -45,9 +47,7 @@ export class FrigateCardThumbnailFeatureEvent extends LitElement {
this,
this._embedThumbnailTask,
(embeddedThumbnail: string | null) =>
embeddedThumbnail
? html`<img src="${embeddedThumbnail}" />`
: html``
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
)
: html`<ha-icon
icon="mdi:image-off"
@@ -86,6 +86,9 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
@property({ attribute: false })
public event?: FrigateEvent;
@property({ attribute: false })
public mediaSeek?: MediaSeek;
protected render(): TemplateResult | void {
if (!this.event) {
return;
@@ -101,6 +104,12 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
<span class="heading">${localize('event.duration')}:</span>
<span>${getEventDurationString(this.event)}</span>
</div>
${this.mediaSeek
? html` <div>
<span class="heading">${localize('event.seek')}</span>
<span>${format(fromUnixTime(this.mediaSeek.seekTime), 'HH:mm:ss')}</span>
</div>`
: html``}
</div>
<div class="right">
<span class="larger">${score}</span>
@@ -117,16 +126,19 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
@property({ attribute: false })
public recording?: FrigateRecording;
@property({ attribute: false })
public mediaSeek?: MediaSeek;
protected render(): TemplateResult | void {
if (!this.recording) {
return;
}
return html`<div class="left">
<div class="larger">${prettifyTitle(this.recording.camera) || ''}</div>
${this.recording.seek_time
${this.mediaSeek
? html` <div>
<span class="heading">${localize('recording.seek')}</span>
<span>${format(fromUnixTime(this.recording.seek_time), 'HH:mm:ss')}</span>
<span>${format(fromUnixTime(this.mediaSeek.seekTime), 'HH:mm:ss')}</span>
</div>`
: html``}
</div>
@@ -161,6 +173,9 @@ export class FrigateCardThumbnail extends LitElement {
@property({ attribute: false })
public childIndex?: number;
@property({ attribute: false })
public mediaSeek?: MediaSeek;
// ===================================================
// Raw interface (can override target-based interface)
// ===================================================
@@ -263,10 +278,12 @@ export class FrigateCardThumbnail extends LitElement {
${this.details && event
? html`<frigate-card-thumbnail-details-event
.event=${event ?? undefined}
.mediaSeek=${this.mediaSeek}
></frigate-card-thumbnail-details-event>`
: this.details && recording
? html`<frigate-card-thumbnail-details-recording
.recording=${recording ?? undefined}
.mediaSeek=${this.mediaSeek}
></frigate-card-thumbnail-details-recording>`
: html``}
${this.show_timeline_control
+615 -644
View File
File diff suppressed because it is too large Load Diff
+40 -13
View File
@@ -55,6 +55,26 @@ import './surround-thumbnails';
import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { TimelineDataManager } from '../utils/timeline-data-manager.js';
export interface MediaSeek {
// Specifies the point at which this recording should be played, the
// seek_time is the date of the desired play point (for display purposes
// usually), and seek_seconds is the number of seconds to seek into the video
// stream to reach that point.
seekTime: number;
seekSeconds: number;
}
export interface MediaViewerViewContext {
seek: Map<number, MediaSeek>;
}
declare module 'view' {
interface ViewContext {
mediaViewer?: MediaViewerViewContext;
}
}
@customElement('frigate-card-viewer')
export class FrigateCardViewer extends LitElement {
@@ -73,6 +93,9 @@ export class FrigateCardViewer extends LitElement {
@property({ attribute: false })
public resolvedMediaCache?: ResolvedMediaCache;
@property({ attribute: false })
public timelineDataManager?: TimelineDataManager;
/**
* Master render method.
* @returns A rendered template.
@@ -114,7 +137,9 @@ export class FrigateCardViewer extends LitElement {
return html` <frigate-card-surround-thumbnails
.hass=${this.hass}
.view=${this.view}
.config=${this.viewerConfig.controls.thumbnails}
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline}
.timelineDataManager=${this.timelineDataManager}
.cameras=${this.cameras}
>
<frigate-card-viewer-carousel
@@ -202,7 +227,7 @@ export class FrigateCardViewerCarousel extends LitElement {
if (oldView) {
if (
oldView.target === this.view?.target &&
this.view.childIndex != oldView.childIndex
oldView.childIndex !== this.view.childIndex
) {
const slide = this._getSlideForChild(this.view.childIndex);
if (
@@ -215,8 +240,14 @@ export class FrigateCardViewerCarousel extends LitElement {
}
}
}
}
// Seek into the video if the seek time has changed (this is also called
// on media load, since the media may or may not have been loaded at
// this point).
if (this.view?.context?.mediaViewer !== oldView?.context?.mediaViewer) {
this._recordingSeekHandler();
}
}
super.updated(changedProperties);
}
@@ -663,15 +694,12 @@ export class FrigateCardViewerCarousel extends LitElement {
* Fire a media show event when a slide is selected.
*/
protected _recordingSeekHandler(): void {
// If this is a recording and play is desired to be started from a
// particular point, seek to that point. Use the media off the slide itself
// -- when the slide is changed, the media show event may be dispatched
// before this.view has been updated to reflect the new selection.
const player = this._getPlayer() as FrigateCardMediaPlayer & {
media?: FrigateBrowseMediaSource;
};
if (player && player.media && player.media.frigate?.recording?.seek_seconds) {
player.seek(player.media.frigate.recording.seek_seconds);
const player = this._getPlayer();
const childIndex = this.view?.childIndex ?? null;
const seek =
childIndex !== null ? this.view?.context?.mediaViewer?.seek.get(childIndex) : null;
if (player && seek) {
player.seek(seek.seekSeconds);
}
}
@@ -718,7 +746,6 @@ export class FrigateCardViewerCarousel extends LitElement {
url=${ifDefined(
lazyLoad ? undefined : this._canonicalizeHAURL(resolvedMedia?.url),
)}
.media=${mediaToRender}
.hass=${this.hass}
@frigate-card:media:loaded=${(e: CustomEvent<MediaLoadedInfo>) => {
wrapMediaLoadedEventForCarousel(slideIndex, e);