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