diff --git a/src/frigate-hass-card.scss b/src/frigate-hass-card.scss index 7c3d41ad..c78d2eaf 100644 --- a/src/frigate-hass-card.scss +++ b/src/frigate-hass-card.scss @@ -80,4 +80,22 @@ webrtc-camera ha-card { box-shadow: none; border-radius: 0px; background-color: var(--secondary-background-color, black); +} + +.frigate-media-controls { + position: absolute; + z-index: 1; + overflow: hidden; + opacity: 0.8; + border-radius: 50%; + top: calc(50% - (54px /2)); + height: 54px; +} + +.frigate-media-controls.previous { + left: 20px; +} + +.frigate-media-controls.next { + right: 20px; } \ No newline at end of file diff --git a/src/frigate-hass-card.ts b/src/frigate-hass-card.ts index bda89c0f..7f6daf8d 100644 --- a/src/frigate-hass-card.ts +++ b/src/frigate-hass-card.ts @@ -34,6 +34,7 @@ import { resolvedMediaSchema, } from './types'; import type { + BrowseMediaNeighbors, BrowseMediaSource, FrigateCardView, FrigateCardConfig, @@ -186,6 +187,41 @@ export class FrigateCardMenu extends LitElement { } } +interface ViewParameters { + view?: FrigateCardView; + target?: BrowseMediaSource; + childIndex?: number; + previous?: View; +} + +class View { + view: FrigateCardView; + target?: BrowseMediaSource; + childIndex?: number; + previous?: View; + + constructor(params?: ViewParameters) { + this.view = params?.view || 'live'; + this.target = params?.target; + this.childIndex = params?.childIndex; + this.previous = params?.previous; + } + + public is(name: string): boolean { + return this.view == name; + } + + get media(): BrowseMediaSource | undefined { + if (this.target) { + if (this.target.children && this.childIndex !== undefined) { + return this.target.children[this.childIndex]; + } + return this.target; + } + return undefined; + } +} + // Main FrigateCard class. @customElement('frigate-card') export class FrigateCard extends LitElement { @@ -212,20 +248,16 @@ export class FrigateCard extends LitElement { @state() public config!: FrigateCardConfig; - @property({ attribute: false }) - protected _viewMode: FrigateCardView = 'live'; - protected _interactionTimerID: number | null = null; protected _webrtcElement: any | null = null; - // A specific media source requested by the user. @property({ attribute: false }) - protected _requestedMediaSource: BrowseMediaSource | null = null; + protected _view: View = new View(); // Media (both browse item & resolved media) actually being shown to the user. - // This may be different from _requestedMediaSource when no particular event is + // This may be different from _view.target when no particular event is // requested (e.g. 'clip' view that views the most recent) -- in that case the - // requestedEvent will be null, but _mediaBeingShown will be the actual event + // _view.target will be null, but _mediaBeingShown will be the actual event // shown. protected _mediaBeingShown: MediaBeingShown | null = null; @@ -320,11 +352,13 @@ export class FrigateCard extends LitElement { // Update the card view. protected _changeView( - view?: FrigateCardView | undefined, - mediaSource?: BrowseMediaSource | undefined, + view?: View | undefined, ): void { - this._viewMode = view || this.config.view_default; - this._requestedMediaSource = mediaSource || null; + if (view === undefined) { + this._view = new View({view: this.config.view_default}) + } else { + this._view = view; + } this._mediaBeingShown = null; } @@ -424,8 +458,11 @@ export class FrigateCard extends LitElement { // Resolve Frigate media identifier to a real URL. protected async _resolveMedia( - mediaSource: BrowseMediaSource, + mediaSource: BrowseMediaSource | null, ): Promise { + if (!mediaSource) { + return null; + } const request = { type: 'media_source/resolve_media', media_content_id: mediaSource.media_content_id, @@ -451,56 +488,62 @@ export class FrigateCard extends LitElement { return this._renderAttentionIcon( 'mdi:alert-circle', html`${ - error ? `${error} .` : `Unknown error` + error ? `${error} .` : `Unknown error .` }Check troubleshooting.`, ); } // Render Frigate events into a card gallery. protected async _renderEvents(): Promise { - const want_clips = this._viewMode == 'clips'; - let media; + let parent; try { - if (this._requestedMediaSource) { - media = await this._browseMedia(this._requestedMediaSource.media_content_id); + if (this._view.target) { + parent = await this._browseMedia(this._view.target.media_content_id); } else { - media = await this._browseMediaQuery(want_clips); + parent = await this._browseMediaQuery(this._view.is('clips')); } } catch (e: any) { return this._renderError(e.message); } - const firstMediaItem = this._getFirstTrueMediaItem(media); - if (!firstMediaItem) { + + if (this._getFirstTrueMediaChildIndex(parent) == null) { return this._renderAttentionIcon( - want_clips ? 'mdi:filmstrip-off' : 'mdi:camera-off', - want_clips ? 'No clips' : 'No snapshots', + this._view.is('clips') ? 'mdi:filmstrip-off' : 'mdi:camera-off', + this._view.is('clips') ? 'No clips' : 'No snapshots', ); } return html`