diff --git a/src/components/gallery.ts b/src/components/gallery.ts index a3cc3c93..30ce89c3 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -22,9 +22,7 @@ import { fetchLatestMediaAndDispatchViewChange, getFullDependentBrowseMediaQueryParametersOrDispatchError, } from '../utils/ha/browse-media'; -import { - changeViewToRecentRecording, -} from '../utils/media-to-view.js'; +import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js'; import { TimelineDataManager } from '../utils/timeline-data-manager'; import { View } from '../view.js'; import { renderProgressIndicator } from './message.js'; @@ -67,7 +65,7 @@ export class FrigateCardGallery extends LitElement { if (!this.view.target) { if (mediaType === 'recordings') { - changeViewToRecentRecording( + changeViewToRecentRecordingForCameraAndDependents( this, this.hass, this.dataManager, @@ -237,7 +235,6 @@ export class FrigateCardGalleryCore extends LitElement { return html``; } - const cameraConfig = this.cameras.get(this.view.camera); return html` ${this._showBackArrow() ? html` { if (this.view) { - this.view - .evolve({ - view: this.view.is('clips') ? 'clip' : 'snapshot', - childIndex: index, - }) - .dispatchChangeEvent(this); + const targetView = this.view.getViewerViewForGalleryView(); + if (targetView) { + this.view + .evolve({ + view: targetView, + childIndex: index, + }) + .dispatchChangeEvent(this); + } } stopEventFromActivatingCardWideActions(ev); }} diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 6cdeea48..1dfc3f39 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -208,7 +208,7 @@ export class FrigateCardThumbnailCarousel extends LitElement { .target=${parent} .childIndex=${childIndex} .mediaSeek=${this.view?.context?.mediaViewer?.seek.get(childIndex)} - .clientID=${cameraConfig?.frigate.client_id} + .cameraConfig=${cameraConfig ?? undefined} ?details=${this.config?.show_details} ?show_favorite_control=${this.config?.show_favorite_control} ?show_timeline_control=${this.config?.show_timeline_control} diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 8468515d..e43b30d6 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -10,6 +10,7 @@ import thumbnailFeatureRecordingStyle from '../scss/thumbnail-feature-recording. import thumbnailStyle from '../scss/thumbnail.scss'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { errorToConsole, prettifyTitle } from '../utils/basic.js'; +import { getCameraTitle } from '../utils/camera.js'; import { retainEvent } from '../utils/frigate.js'; import { getEventDurationString } from '../utils/frigate.js'; import { renderTask } from '../utils/task.js'; @@ -19,6 +20,7 @@ import { MediaSeek } from './viewer.js'; import { TaskStatus } from '@lit-labs/task'; import type { + CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource, FrigateEvent, @@ -108,6 +110,9 @@ export class FrigateCardThumbnailFeatureRecording extends LitElement { @property({ attribute: false }) public date?: Date; + @property({ attribute: false }) + public cameraTitle?: string; + protected render(): TemplateResult | void { if (!this.date) { return; @@ -115,6 +120,7 @@ export class FrigateCardThumbnailFeatureRecording extends LitElement { return html`
${format(this.date, 'HH:mm')}
${format(this.date, 'MMM do')}
+ ${this.cameraTitle ? html`
${this.cameraTitle}
` : html``} `; } @@ -240,7 +246,7 @@ export class FrigateCardThumbnail extends LitElement { public hass?: ExtendedHomeAssistant; @property({ attribute: false }) - public clientID?: string; + public cameraConfig?: CameraConfig; /** * Render the element. @@ -277,6 +283,7 @@ export class FrigateCardThumbnail extends LitElement { starred: !!event?.retain_indefinitely, }; + const clientID = this.cameraConfig?.frigate.client_id; return html` ${event ? html`` - : html``} - ${this.show_favorite_control && event && this.hass && this.clientID + >` + : html``} + ${this.show_favorite_control && event && this.hass && clientID ? html` { stopEventFromActivatingCardWideActions(ev); - if (event && this.hass && this.clientID) { + if (event && this.hass && clientID) { retainEvent( this.hass, - this.clientID, + clientID, event.id, !event.retain_indefinitely, ) diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 8cf0e48d..ebb1d37b 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -1024,6 +1024,7 @@ export class FrigateCardTimelineCore extends LitElement { } const options = this._getOptions(); + let createdTimeline = false; if ( this.timelineDataManager && @@ -1056,6 +1057,7 @@ export class FrigateCardTimelineCore extends LitElement { this.timelineConfig.media, ); + createdTimeline = true; if (this.mini && groups.length === 1) { // In a mini timeline, if there's only one group don't bother grouping // at all. @@ -1097,7 +1099,15 @@ export class FrigateCardTimelineCore extends LitElement { } if (changedProperties.has('view')) { - this._updateTimelineFromView(); + if (createdTimeline) { + // If the timeline was just created, give it one frame to draw itself. + // Failure to do so may result in subsequent calls to + // `this._timeline.setwindow()` being entirely ignored. Example case: + // Clicking the timeline control on a recording thumbnail. + window.requestAnimationFrame(this._updateTimelineFromView.bind(this)); + } else { + this._updateTimelineFromView(); + } } } diff --git a/src/components/viewer.ts b/src/components/viewer.ts index a2f73302..465c8241 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -56,7 +56,7 @@ 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'; -import { changeViewToRecentRecording } from '../utils/media-to-view.js'; +import { changeViewToRecentRecordingForCameraAndDependents } from '../utils/media-to-view.js'; export interface MediaSeek { // Specifies the point at which this recording should be played, the @@ -131,7 +131,7 @@ export class FrigateCardViewer extends LitElement { } if (mediaType === 'recordings') { - changeViewToRecentRecording( + changeViewToRecentRecordingForCameraAndDependents( this, this.hass, this.timelineDataManager, diff --git a/src/scss/thumbnail-feature-recording.scss b/src/scss/thumbnail-feature-recording.scss index 62907313..dab70503 100644 --- a/src/scss/thumbnail-feature-recording.scss +++ b/src/scss/thumbnail-feature-recording.scss @@ -21,7 +21,9 @@ div { text-align: center; } - div.title { font-size: 1.5rem; } +div.camera { + font-size: 0.7em; +} diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts index 41ee43ae..3874872b 100644 --- a/src/utils/media-to-view.ts +++ b/src/utils/media-to-view.ts @@ -12,6 +12,7 @@ import { sortSegmentsOldestToYoungest, TimelineDataManager, } from './timeline-data-manager'; +import { getAllDependentCameras } from './camera.js'; /** * Change the view to a recent recording. @@ -20,17 +21,15 @@ import { * @param dataManager The datamanager to use for data access. * @param cameras The camera configurations. * @param view The current view. - * @param cameraIDs The camera IDs to include recordings for. - * @param options A specific window (start and end) to fetch recordings for, and a targetTime to seek to. + * @param options A set of cameraIDs to fetch recordings for, and a targetView to dispatch to. */ - export const changeViewToRecentRecording = async ( + export const changeViewToRecentRecordingForCameraAndDependents = async ( element: HTMLElement, hass: ExtendedHomeAssistant, dataManager: TimelineDataManager, cameras: Map, view: View, options?: { - cameraIDs?: Set; targetView?: 'recording' | 'recordings'; }, ): Promise => { @@ -46,6 +45,7 @@ import { ...options, // Fetch 1 days worth of recordings (including recordings that are for the current hour). + cameraIDs: getAllDependentCameras(cameras, view.camera), start: sub(now, { days: 1 }), end: add(now, { hours: 1 }), } @@ -59,8 +59,9 @@ import { * @param dataManager The datamanager to use for data access. * @param cameras The camera configurations. * @param view The current view. - * @param cameraIDs The camera IDs to include recordings for. - * @param options A specific window (start and end) to fetch recordings for, and a targetTime to seek to. + * @param options A specific window (start and end) to fetch recordings for, a + * targetTime to seek to, a targetView to dispatch to and a set of cameraIDs to + * restrict to. */ export const changeViewToRecording = async ( element: HTMLElement, diff --git a/src/view.ts b/src/view.ts index 922b19e0..3d82ce5d 100644 --- a/src/view.ts +++ b/src/view.ts @@ -150,6 +150,20 @@ export class View { return ['clips', 'snapshots', 'recordings'].includes(this.view); } + /** + * Get the viewer view given a gallery view. + */ + public getViewerViewForGalleryView(): 'clip' | 'snapshot' | 'recording' | null { + if (this.is('clips')) { + return 'clip'; + } else if (this.is('snapshots')) { + return 'snapshot'; + } else if (this.is('recordings')) { + return 'recording'; + } + return null; + } + /** * Determine if a view is of a piece of media (including the media viewer, * live view, image view -- anything that can create a MediaLoadedInfo event).