Initial mini-timeline commit.
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+40
-13
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user