diff --git a/src/frigate-hass-card-menu.scss b/src/frigate-hass-card-menu.scss index 4a1607da..efbbff40 100644 --- a/src/frigate-hass-card-menu.scss +++ b/src/frigate-hass-card-menu.scss @@ -13,7 +13,7 @@ .frigate-card-menu-hidden { @extend .frigate-card-menu-base; position: absolute; - width: 50px; + width: 56px; } /* Expanded overlay menu */ diff --git a/src/frigate-hass-card.scss b/src/frigate-hass-card.scss index 7c3d41ad..cecaf39d 100644 --- a/src/frigate-hass-card.scss +++ b/src/frigate-hass-card.scss @@ -51,6 +51,7 @@ ha-card .frigate-card-image-list-folder { box-sizing: border-box; text-align: center; border-style: 2px solid; + opacity: 0.7; color: var(--secondary-text-color, white); border-color: var(--secondary-text-color, black); background-color: var(--primary-background-color, black); @@ -80,4 +81,23 @@ 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% - (48px /2)); + height: 48px; + box-shadow: 0px 0px 30px 1px black; +} + +.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..6c08c272 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; @@ -319,12 +351,12 @@ export class FrigateCard extends LitElement { } // Update the card view. - protected _changeView( - view?: FrigateCardView | undefined, - mediaSource?: BrowseMediaSource | undefined, - ): void { - this._viewMode = view || this.config.view_default; - this._requestedMediaSource = mediaSource || null; + protected _changeView(view?: View | undefined): void { + if (view === undefined) { + this._view = new View({ view: this.config.view_default }); + } else { + this._view = view; + } this._mediaBeingShown = null; } @@ -424,8 +456,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 +486,85 @@ 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`