Add support for both snapshots/clips in timeline.
This commit is contained in:
+31
-18
@@ -1,6 +1,7 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
|
||||
import type {
|
||||
BrowseMediaQueryParametersBase,
|
||||
BrowseMediaQueryParameters,
|
||||
FrigateBrowseMediaSource,
|
||||
CameraConfig,
|
||||
@@ -39,8 +40,8 @@ export class BrowseMediaUtil {
|
||||
* @param media The media object.
|
||||
* @returns `true` if it's truly a media item, `false` otherwise.
|
||||
*/
|
||||
static isTrueMedia(media: FrigateBrowseMediaSource): boolean {
|
||||
return !media.can_expand;
|
||||
static isTrueMedia(media?: FrigateBrowseMediaSource): boolean {
|
||||
return !!media && !media.can_expand;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,15 +114,13 @@ export class BrowseMediaUtil {
|
||||
* Get the parameters to search for media.
|
||||
* @returns A BrowseMediaQueryParameters object.
|
||||
*/
|
||||
static getBrowseMediaQueryParameters(
|
||||
mediaType: 'clips' | 'snapshots',
|
||||
static getBrowseMediaQueryParametersBase(
|
||||
cameraConfig?: CameraConfig,
|
||||
): BrowseMediaQueryParameters | null {
|
||||
): BrowseMediaQueryParametersBase | null {
|
||||
if (!cameraConfig || !cameraConfig.camera_name) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
mediaType: mediaType,
|
||||
clientId: cameraConfig.client_id,
|
||||
cameraName: cameraConfig.camera_name,
|
||||
label: cameraConfig.label,
|
||||
@@ -129,20 +128,37 @@ export class BrowseMediaUtil {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mediaType parameter from the current view.
|
||||
* @param browseMediaQueryParametersBase The base media query parameters object.
|
||||
* @param view The current view.
|
||||
* @returns A fully populated BrowseMediaQueryParameters or null.
|
||||
*/
|
||||
static setMediaTypeFromView(
|
||||
browseMediaQueryParametersBase: BrowseMediaQueryParametersBase | null,
|
||||
view: View,
|
||||
): BrowseMediaQueryParameters | null {
|
||||
if (
|
||||
!browseMediaQueryParametersBase ||
|
||||
!(view.isClipRelatedView() || view.isSnapshotRelatedView())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...browseMediaQueryParametersBase,
|
||||
mediaType: view.isClipRelatedView() ? 'clips' : 'snapshots',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters to search for media related to the current view.
|
||||
* @returns A BrowseMediaQueryParameters object.
|
||||
*/
|
||||
static getBrowseMediaQueryParametersOrDispatchError(
|
||||
static getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
node: HTMLElement,
|
||||
view: View,
|
||||
cameraConfig: CameraConfig,
|
||||
): BrowseMediaQueryParameters | null {
|
||||
if (!view.isClipRelatedView() && !view.isSnapshotRelatedView()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify there is a camera name, otherwise getBrowseMediaQueryParameters()
|
||||
): BrowseMediaQueryParametersBase | null {
|
||||
// Verify there is a camera name, otherwise getBrowseMediaQueryParametersBase()
|
||||
// will return undefined.
|
||||
if (!cameraConfig.camera_name) {
|
||||
dispatchErrorMessageEvent(
|
||||
@@ -152,10 +168,7 @@ export class BrowseMediaUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
return BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||
view.isClipRelatedView() ? 'clips' : 'snapshots',
|
||||
cameraConfig,
|
||||
);
|
||||
return BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-2
@@ -1089,13 +1089,15 @@ export class FrigateCard extends LitElement {
|
||||
// Do not artifically constrain aspect ratio if:
|
||||
// - It's fullscreen.
|
||||
// - Aspect ratio enforcement is disabled.
|
||||
// - Aspect ratio enforcement is dynamic and it's a media view (i.e. not the gallery).
|
||||
// - Aspect ratio enforcement is dynamic and it's a media view (i.e. not the
|
||||
// gallery) or timeline.
|
||||
// - There is a message to display to the user.
|
||||
|
||||
return !(
|
||||
(screenfull.isEnabled && screenfull.isFullscreen) ||
|
||||
aspectRatioMode == 'unconstrained' ||
|
||||
(aspectRatioMode == 'dynamic' && this._view?.isMediaView()) ||
|
||||
(aspectRatioMode == 'dynamic' &&
|
||||
(this._view?.isMediaView() || this._view?.is('timeline'))) ||
|
||||
this._message != null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,16 +38,17 @@ export class FrigateCardGallery extends LitElement {
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.view || !this.cameraConfig) {
|
||||
if (!this.hass || !this.view || !this.cameraConfig || !this.view.isGalleryView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.view.target) {
|
||||
const browseMediaQueryParameters =
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersOrDispatchError(
|
||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
this,
|
||||
this.view,
|
||||
this.cameraConfig,
|
||||
),
|
||||
this.view,
|
||||
);
|
||||
if (!browseMediaQueryParameters) {
|
||||
return;
|
||||
@@ -207,7 +208,7 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
class="mdc-image-list__image"
|
||||
src="${child.thumbnail}"
|
||||
title="${child.title}"
|
||||
@click=${(ev) => {
|
||||
@click=${(ev: Event) => {
|
||||
if (this.view) {
|
||||
this.view
|
||||
.evolve({
|
||||
@@ -219,11 +220,13 @@ export class FrigateCardGalleryCore extends LitElement {
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
/>${child.frigate?.event?.retain_indefinitely ? html`<ha-icon
|
||||
/>${child.frigate?.event?.retain_indefinitely
|
||||
? html`<ha-icon
|
||||
class="favorite"
|
||||
icon="mdi:star"
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
/>` : ``}`
|
||||
/>`
|
||||
: ``}`
|
||||
: ``}
|
||||
</div>
|
||||
</li>`,
|
||||
|
||||
+8
-14
@@ -7,7 +7,6 @@ import {
|
||||
PropertyValues,
|
||||
} from 'lit';
|
||||
import {
|
||||
FrigateBrowseMediaSource,
|
||||
ExtendedHomeAssistant,
|
||||
CameraConfig,
|
||||
JSMPEGConfig,
|
||||
@@ -20,25 +19,21 @@ import {
|
||||
LiveProvider,
|
||||
TransitionEffect,
|
||||
frigateCardConfigDefaults,
|
||||
BrowseMediaQueryParameters,
|
||||
} from '../types.js';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js';
|
||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
|
||||
import { FrigateCardMediaCarousel } from './media-carousel.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import {
|
||||
FrigateCardThumbnailCarousel,
|
||||
ThumbnailCarouselTap,
|
||||
} from './thumbnail-carousel.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import { View } from '../view.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
@@ -104,9 +99,6 @@ export class FrigateCardLive extends LitElement {
|
||||
// pre-loading it may be propagated upwards later.
|
||||
protected _savedMediaShowInfo?: MediaShowInfo;
|
||||
|
||||
@query('frigate-card-thumbnail-carousel')
|
||||
protected _thumbnailCarousel?: FrigateCardThumbnailCarousel;
|
||||
|
||||
/**
|
||||
* Handler for media show events that special cases preloaded live views.
|
||||
* @param e The media show event.
|
||||
@@ -135,13 +127,16 @@ export class FrigateCardLive extends LitElement {
|
||||
this.conditionState,
|
||||
) as LiveConfig;
|
||||
|
||||
const browseMediaParams = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||
config.controls.thumbnails.media,
|
||||
const browseMediaParamsBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
|
||||
this.cameras.get(this.view.camera),
|
||||
);
|
||||
if (!browseMediaParams) {
|
||||
if (!browseMediaParamsBase) {
|
||||
return;
|
||||
}
|
||||
const browseMediaParams: BrowseMediaQueryParameters = {
|
||||
...browseMediaParamsBase,
|
||||
mediaType: config.controls.thumbnails.media,
|
||||
}
|
||||
|
||||
// Note use of liveConfig and not config below -- the carousel will
|
||||
// independently override the liveconfig to reflect the camera in the
|
||||
@@ -150,7 +145,6 @@ export class FrigateCardLive extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.config=${config.controls.thumbnails}
|
||||
.targetView=${config.controls.thumbnails.media == 'clips' ? 'clip' : 'snapshot'}
|
||||
.browseMediaParams=${browseMediaParams}
|
||||
>
|
||||
<frigate-card-live-carousel
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||
import {
|
||||
@@ -45,7 +45,8 @@ export class FrigateCardSurround extends LitElement {
|
||||
]);
|
||||
|
||||
/**
|
||||
* Fetch thumbnail media.
|
||||
* Fetch thumbnail media when a target is not specified in the view (e.g. for
|
||||
* the live view).
|
||||
* @param param Task parameters.
|
||||
* @returns
|
||||
*/
|
||||
@@ -121,7 +122,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||
this.view
|
||||
?.evolve({
|
||||
...(this.targetView && { view: this.targetView }),
|
||||
view: this.targetView || 'event',
|
||||
target: ev.detail.target,
|
||||
childIndex: ev.detail.childIndex,
|
||||
})
|
||||
|
||||
+89
-51
@@ -1,5 +1,4 @@
|
||||
// TODO: Clips vs snapshots: Should be able to navigate from snapshots view and it should just work.
|
||||
// TODO: Remove HACK in view.ts on clips
|
||||
// TODO: Hover over an event should show something useful.
|
||||
// TODO: Periodically refetch events.
|
||||
// TODO: Search for TODOs and logging statements.
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
import { DataSet } from 'vis-data/esnext';
|
||||
import {
|
||||
DataGroupCollectionType,
|
||||
IdType,
|
||||
Timeline,
|
||||
TimelineItem,
|
||||
TimelineOptions,
|
||||
@@ -36,8 +34,7 @@ import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
TimelineConfig,
|
||||
FrigateEvent,
|
||||
} from '../types';
|
||||
@@ -58,7 +55,9 @@ interface FrigateCardGroupData {
|
||||
content: string;
|
||||
}
|
||||
interface FrigateCardTimelineItem extends TimelineItem {
|
||||
source: FrigateBrowseMediaSource;
|
||||
event: FrigateEvent;
|
||||
clip?: FrigateBrowseMediaSource;
|
||||
snapshot?: FrigateBrowseMediaSource;
|
||||
}
|
||||
|
||||
interface TimelineViewContext extends ViewContext {
|
||||
@@ -114,20 +113,29 @@ class TimelineEventManager {
|
||||
protected _addMediaSource(camera: string, target: FrigateBrowseMediaSource): void {
|
||||
const items: FrigateCardTimelineItem[] = [];
|
||||
target.children?.forEach((child) => {
|
||||
if (child.frigate) {
|
||||
const item = {
|
||||
id: child.media_content_id,
|
||||
const event = child.frigate?.event;
|
||||
if (event && ['video', 'image'].includes(child.media_content_type)) {
|
||||
let item = this._dataset.get(event.id);
|
||||
if (!item) {
|
||||
item = {
|
||||
id: event.id,
|
||||
group: camera,
|
||||
content: this._contentCallback?.(child) ?? '',
|
||||
start: child.frigate.event.start_time * 1000,
|
||||
source: child,
|
||||
start: event.start_time * 1000,
|
||||
event: event,
|
||||
};
|
||||
if (child.frigate.event.end_time) {
|
||||
item['end'] = child.frigate.event.end_time * 1000;
|
||||
}
|
||||
if (event.end_time) {
|
||||
item['end'] = event.end_time * 1000;
|
||||
item['type'] = 'range';
|
||||
} else {
|
||||
item['type'] = 'point';
|
||||
}
|
||||
if (child.media_content_type === 'video') {
|
||||
item['clip'] = child;
|
||||
} else if (child.media_content_type === 'image') {
|
||||
item['snapshot'] = child;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
@@ -161,13 +169,14 @@ class TimelineEventManager {
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
media: 'all' | 'clips' | 'snapshots',
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<boolean> {
|
||||
if (this.hasCoverage(start, end)) {
|
||||
return false;
|
||||
}
|
||||
await this._fetchEvents(element, hass, cameras, start, end);
|
||||
await this._fetchEvents(element, hass, cameras, media, start, end);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -183,6 +192,7 @@ class TimelineEventManager {
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant & ExtendedHomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
media: 'all' | 'clips' | 'snapshots',
|
||||
start: Date,
|
||||
end: Date,
|
||||
): Promise<void> {
|
||||
@@ -193,24 +203,25 @@ class TimelineEventManager {
|
||||
this._dateEnd = end;
|
||||
}
|
||||
|
||||
const fetchCameraEvents = async (camera: string): Promise<void> => {
|
||||
const fetchCameraEvents = async (
|
||||
camera: string,
|
||||
mediaType: 'clips' | 'snapshots',
|
||||
): Promise<void> => {
|
||||
const cameraConfig = cameras.get(camera);
|
||||
if (!cameraConfig || !this._dateStart || !this._dateEnd) {
|
||||
return;
|
||||
}
|
||||
const browseMediaQueryParameters = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||
'clips',
|
||||
const browseMediaQueryParametersBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
|
||||
cameraConfig,
|
||||
);
|
||||
if (!browseMediaQueryParameters) {
|
||||
if (!browseMediaQueryParametersBase) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._addMediaSource(
|
||||
camera,
|
||||
await BrowseMediaUtil.browseMediaQuery(hass, {
|
||||
...browseMediaQueryParameters,
|
||||
...browseMediaQueryParametersBase,
|
||||
|
||||
// Events are always fetched for the maximum extent of the managed
|
||||
// range. This is because events may change at any point in time
|
||||
@@ -218,6 +229,7 @@ class TimelineEventManager {
|
||||
before: this._dateEnd.getTime() / 1000,
|
||||
after: this._dateStart.getTime() / 1000,
|
||||
unlimited: true,
|
||||
mediaType: mediaType,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -225,7 +237,15 @@ class TimelineEventManager {
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(Array.from(cameras.keys()).map(fetchCameraEvents.bind(this)));
|
||||
const promises: Promise<void>[] = [];
|
||||
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) =>
|
||||
promises.push(
|
||||
...Array.from(cameras.keys()).map((camera) =>
|
||||
fetchCameraEvents(camera, mediaType as 'clips' | 'snapshots'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +276,6 @@ export class FrigateCardTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.config=${this.timelineConfig.controls.thumbnails}
|
||||
.targetView=${'clip'}
|
||||
>
|
||||
<frigate-card-timeline-core
|
||||
.hass=${this.hass}
|
||||
@@ -330,12 +349,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
console.info(
|
||||
`Range changed: ${properties.start} -> ${properties.end} [${this._events.dataset.length}]`,
|
||||
);
|
||||
if (this.hass && this.cameras && this._timeline) {
|
||||
if (this.hass && this.cameras && this._timeline && this.timelineConfig) {
|
||||
this._events
|
||||
.fetchEventsIfNecessary(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.timelineConfig.media,
|
||||
properties.start,
|
||||
properties.end,
|
||||
)
|
||||
@@ -367,7 +387,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
if (!this._thumbnails || !this._thumbnails.children || data.items.length <= 0) {
|
||||
return;
|
||||
}
|
||||
const childIndex = this._findThumbnailIndex(data.items[0]);
|
||||
const childIndex = this._thumbnails.children.findIndex(
|
||||
(child) => child.frigate?.event.id === data.items[0],
|
||||
);
|
||||
if (childIndex >= 0) {
|
||||
this.view
|
||||
?.evolve({
|
||||
@@ -379,19 +401,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the index of the given item in the thumbnails.
|
||||
* @param id
|
||||
* @returns The index of the item, or -1 if not found.
|
||||
*/
|
||||
public _findThumbnailIndex(id: IdType | IdType[]): number {
|
||||
if (!this._thumbnails || !this._thumbnails.children) {
|
||||
return -1;
|
||||
}
|
||||
id = Array.isArray(id) ? id[0] : id;
|
||||
return this._thumbnails.children.findIndex((child) => child.media_content_id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate the thumbnails from the timeline events.
|
||||
* @returns
|
||||
@@ -401,10 +410,33 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const children: FrigateBrowseMediaSource[] = this._events.dataset
|
||||
.get()
|
||||
.filter((item) => BrowseMediaUtil.isTrueMedia(item.source))
|
||||
.map((item) => item.source);
|
||||
const selected = this._timeline.getSelection();
|
||||
let childIndex = -1;
|
||||
const children: FrigateBrowseMediaSource[] = [];
|
||||
this._events.dataset.get().forEach((item) => {
|
||||
if (this.timelineConfig) {
|
||||
let added = false;
|
||||
if (
|
||||
item.clip &&
|
||||
['all', 'clips'].includes(this.timelineConfig.media) &&
|
||||
BrowseMediaUtil.isTrueMedia(item.clip)
|
||||
) {
|
||||
added = true;
|
||||
children.push(item.clip);
|
||||
} else if (
|
||||
item.snapshot &&
|
||||
['all', 'snapshots'].includes(this.timelineConfig.media) &&
|
||||
BrowseMediaUtil.isTrueMedia(item.snapshot)
|
||||
) {
|
||||
added = true
|
||||
children.push(item.snapshot);
|
||||
}
|
||||
|
||||
if (added && selected.includes(item.event.id)) {
|
||||
childIndex = children.length-1;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!children.length) {
|
||||
return;
|
||||
}
|
||||
@@ -412,17 +444,16 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
const target = {
|
||||
title: `Timeline events`,
|
||||
media_class: MEDIA_CLASS_PLAYLIST,
|
||||
media_content_type: MEDIA_TYPE_VIDEO,
|
||||
media_content_type: MEDIA_TYPE_PLAYLIST,
|
||||
media_content_id: '',
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: MEDIA_CLASS_VIDEO,
|
||||
children_media_class: MEDIA_CLASS_PLAYLIST,
|
||||
thumbnail: null,
|
||||
children: children,
|
||||
};
|
||||
|
||||
this._thumbnails = target;
|
||||
const childIndex = this._findThumbnailIndex(this._timeline.getSelection());
|
||||
|
||||
// Update the thumbnail carousel with the regenerated thumbnails.
|
||||
this.view
|
||||
@@ -523,11 +554,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
// different object types together (e.g. person and car).
|
||||
return (
|
||||
!!first.id &&
|
||||
first.id !== this.view?.media?.media_content_id &&
|
||||
first.id !== this.view?.media?.frigate?.event?.id &&
|
||||
!!second.id &&
|
||||
second.id != this.view?.media?.media_content_id &&
|
||||
(<FrigateCardTimelineItem>first).source.frigate?.event.label ===
|
||||
(<FrigateCardTimelineItem>second).source.frigate?.event.label
|
||||
second.id != this.view?.media?.frigate?.event?.id &&
|
||||
(<FrigateCardTimelineItem>first).event.label ===
|
||||
(<FrigateCardTimelineItem>second).event.label
|
||||
);
|
||||
},
|
||||
}
|
||||
@@ -573,9 +604,15 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
*/
|
||||
protected async _updateTimelineFromView(): Promise<void> {
|
||||
const event = this.view?.media?.frigate?.event;
|
||||
const id = this.view?.media?.media_content_id;
|
||||
|
||||
if (!this.hass || !this.cameras || !this.view || !event || !id || !this._timeline) {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.cameras ||
|
||||
!this.view ||
|
||||
!event ||
|
||||
!this._timeline ||
|
||||
!this.timelineConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -585,6 +622,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
this,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.timelineConfig.media,
|
||||
eventWindowStart,
|
||||
eventWindowEnd,
|
||||
)
|
||||
@@ -595,7 +633,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
const eventStart = new Date(event.start_time * 1000);
|
||||
const eventEnd = event.end_time ? new Date(event.end_time * 1000) : 0;
|
||||
|
||||
this._timeline.setSelection([id], {
|
||||
this._timeline.setSelection([event.id], {
|
||||
focus: false,
|
||||
animation: {
|
||||
animation: false,
|
||||
@@ -628,7 +666,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
// Hack: Clustering may not update unless the dataset changes, artifically
|
||||
// update the dataset to ensure the newly selected item cannot be included
|
||||
// in a cluster.
|
||||
const item = this._events.dataset.get(id);
|
||||
const item = this._events.dataset.get(event.id);
|
||||
if (item) {
|
||||
this._events.dataset.updateOnly(item);
|
||||
}
|
||||
|
||||
+17
-20
@@ -10,14 +10,14 @@ import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import type {
|
||||
BrowseMediaNeighbors,
|
||||
BrowseMediaQueryParameters,
|
||||
BrowseMediaQueryParametersBase,
|
||||
FrigateBrowseMediaSource,
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
TransitionEffect,
|
||||
ViewerConfig,
|
||||
} from '../types.js';
|
||||
import { CarouselSelect } from './carousel.js';
|
||||
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
contentsChanged,
|
||||
createMediaShowInfo,
|
||||
dispatchErrorMessageEvent,
|
||||
dispatchFrigateCardEvent,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../common.js';
|
||||
import { renderProgressIndicator } from '../components/message.js';
|
||||
@@ -71,17 +69,21 @@ export class FrigateCardViewer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const browseMediaQueryParameters =
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersOrDispatchError(
|
||||
const browseMediaQueryParametersBase =
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
this,
|
||||
this.view,
|
||||
this.cameraConfig,
|
||||
);
|
||||
|
||||
if (!this.view.target) {
|
||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
||||
browseMediaQueryParametersBase,
|
||||
this.view,
|
||||
);
|
||||
if (!browseMediaQueryParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.view.target) {
|
||||
BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange(
|
||||
this,
|
||||
this.hass,
|
||||
@@ -95,13 +97,12 @@ export class FrigateCardViewer extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.config=${this.viewerConfig.controls.thumbnails}
|
||||
.browseMediaParams=${browseMediaQueryParameters}
|
||||
>
|
||||
<frigate-card-viewer-carousel
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
||||
.browseMediaQueryParametersBase=${browseMediaQueryParametersBase}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
>
|
||||
</frigate-card-viewer-carousel>
|
||||
@@ -133,7 +134,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
protected viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected browseMediaQueryParameters?: BrowseMediaQueryParameters;
|
||||
protected browseMediaQueryParametersBase?: BrowseMediaQueryParametersBase;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
@@ -267,16 +268,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
? this._lazyloadSlide.bind(this)
|
||||
: undefined,
|
||||
}),
|
||||
// Don't need autoplay/pause for snapshots.
|
||||
...(this.view?.is('clip')
|
||||
? [
|
||||
AutoMediaPlugin({
|
||||
playerSelector: 'frigate-card-ha-hls-player',
|
||||
autoPlayWhenVisible: !!this.viewerConfig?.auto_play,
|
||||
autoUnmuteWhenVisible: !!this.viewerConfig?.auto_unmute,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -338,7 +334,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!this.view.target.children.length ||
|
||||
!this.browseMediaQueryParameters
|
||||
!this.browseMediaQueryParametersBase
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -381,7 +377,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
|
||||
try {
|
||||
clips = await BrowseMediaUtil.browseMediaQuery(this.hass, {
|
||||
...this.browseMediaQueryParameters,
|
||||
...this.browseMediaQueryParametersBase,
|
||||
mediaType: 'clips',
|
||||
before: latest,
|
||||
after: earliest,
|
||||
@@ -628,7 +624,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
if (
|
||||
!this.view ||
|
||||
!this.viewerConfig ||
|
||||
!BrowseMediaUtil.isTrueMedia(mediaToRender)
|
||||
!BrowseMediaUtil.isTrueMedia(mediaToRender) ||
|
||||
!['video', 'image'].includes(mediaToRender.media_content_type)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -641,7 +638,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
${this.view.isClipRelatedView()
|
||||
${mediaToRender.media_content_type === 'video'
|
||||
? html`<frigate-card-ha-hls-player
|
||||
allow-exoplayer
|
||||
aria-label="${mediaToRender.title}"
|
||||
|
||||
@@ -10,7 +10,3 @@ frigate-card-live-carousel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
frigate-card-thumbnail-carousel {
|
||||
flex: 0 0 var(--frigate-card-carousel-thumbnail-size);
|
||||
}
|
||||
+17
-5
@@ -35,7 +35,14 @@ const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
|
||||
'timeline',
|
||||
] as const;
|
||||
|
||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number];
|
||||
const FRIGATE_CARD_VIEWS = [
|
||||
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||
|
||||
// Event: A clip or snapshot (timeline may produce mixed media lists).
|
||||
'event',
|
||||
] as const;
|
||||
|
||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
||||
|
||||
const FRIGATE_MENU_MODES = [
|
||||
'none',
|
||||
@@ -755,6 +762,7 @@ const dimensionsConfigSchema = z
|
||||
*/
|
||||
const timelineConfigDefault = {
|
||||
clustering_threshold: 3,
|
||||
media: 'all' as const,
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'left' as const,
|
||||
@@ -766,6 +774,7 @@ const timelineConfigDefault = {
|
||||
const timelineConfigSchema = z
|
||||
.object({
|
||||
clustering_threshold: z.number().default(timelineConfigDefault.clustering_threshold),
|
||||
media: z.enum(['all', 'clips', 'snapshots']).default(timelineConfigDefault.media),
|
||||
controls: z
|
||||
.object({
|
||||
thumbnails: thumbnailsControlSchema
|
||||
@@ -867,8 +876,8 @@ export interface ExtendedHomeAssistant {
|
||||
hassUrl(path?): string;
|
||||
}
|
||||
|
||||
export interface BrowseMediaQueryParameters {
|
||||
mediaType: 'clips' | 'snapshots';
|
||||
export interface BrowseMediaQueryParametersBase {
|
||||
mediaType?: 'clips' | 'snapshots';
|
||||
clientId: string;
|
||||
cameraName: string;
|
||||
label?: string;
|
||||
@@ -878,6 +887,10 @@ export interface BrowseMediaQueryParameters {
|
||||
unlimited?: boolean;
|
||||
}
|
||||
|
||||
export interface BrowseMediaQueryParameters extends BrowseMediaQueryParametersBase {
|
||||
mediaType: 'clips' | 'snapshots';
|
||||
}
|
||||
|
||||
export interface BrowseMediaNeighbors {
|
||||
previous: FrigateBrowseMediaSource | null;
|
||||
previousIndex: number | null;
|
||||
@@ -920,8 +933,7 @@ export interface FrigateCardMediaPlayer {
|
||||
*/
|
||||
|
||||
export const MEDIA_CLASS_PLAYLIST = 'playlist' as const;
|
||||
export const MEDIA_CLASS_VIDEO = 'video' as const;
|
||||
export const MEDIA_TYPE_VIDEO = 'video' as const;
|
||||
export const MEDIA_TYPE_PLAYLIST = 'playlist' as const;
|
||||
|
||||
// Recursive type, cannot use type interference:
|
||||
// See: https://github.com/colinhacks/zod#recursive-types
|
||||
|
||||
+3
-4
@@ -83,22 +83,21 @@ export class View {
|
||||
* Determine if a view is of a piece of media (i.e. not the gallery).
|
||||
*/
|
||||
public isMediaView(): boolean {
|
||||
return !this.isGalleryView();
|
||||
return this.isViewerView() || this.is('live');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a view is for the media viewer.
|
||||
*/
|
||||
public isViewerView(): boolean {
|
||||
return ['clip', 'snapshot'].includes(this.view);
|
||||
return ['clip', 'snapshot', 'event'].includes(this.view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a view is related to a clip or clips.
|
||||
*/
|
||||
public isClipRelatedView(): boolean {
|
||||
// TODO HACK HACK HACK
|
||||
return ['clip', 'clips', 'timeline'].includes(this.view);
|
||||
return ['clip', 'clips'].includes(this.view);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user