Set timeline to now not to most recent event.
This commit is contained in:
@@ -0,0 +1,224 @@
|
|||||||
|
import sub from 'date-fns/sub';
|
||||||
|
import { LitElement, ReactiveController } from 'lit';
|
||||||
|
import { ViewContext } from 'view';
|
||||||
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
|
import { FrigateCardMessageEventTarget } from '../../components/message.js';
|
||||||
|
import { CardWideConfig, LiveConfig } from '../../config/types.js';
|
||||||
|
import { MediaLoadedInfo, Message } from '../../types.js';
|
||||||
|
import {
|
||||||
|
FrigateCardMediaLoadedEventTarget,
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
|
} from '../../utils/media-info.js';
|
||||||
|
import {
|
||||||
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
|
changeViewToRecentRecordingForCameraAndDependents,
|
||||||
|
} from '../../utils/media-to-view.js';
|
||||||
|
import { FrigateCardViewChangeEventTarget, View } from '../../view/view.js';
|
||||||
|
|
||||||
|
interface LiveViewContext {
|
||||||
|
// A cameraID override (used for dependencies/substreams to force a different
|
||||||
|
// camera to be live rather than the camera selected in the view).
|
||||||
|
overrides?: Map<string, string>;
|
||||||
|
|
||||||
|
ptzVisible?: boolean;
|
||||||
|
fetchThumbnails?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'view' {
|
||||||
|
interface ViewContext {
|
||||||
|
live?: LiveViewContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LastMediaLoadedInfo {
|
||||||
|
mediaLoadedInfo: MediaLoadedInfo;
|
||||||
|
source: EventTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveControllerHost = LitElement &
|
||||||
|
FrigateCardMediaLoadedEventTarget &
|
||||||
|
FrigateCardMessageEventTarget &
|
||||||
|
FrigateCardViewChangeEventTarget;
|
||||||
|
|
||||||
|
export class LiveController implements ReactiveController {
|
||||||
|
protected _host: LiveControllerHost;
|
||||||
|
|
||||||
|
// Whether or not the live view is currently in the background (i.e. preloaded
|
||||||
|
// but not visible).
|
||||||
|
protected _inBackground = false;
|
||||||
|
|
||||||
|
// Intersection handler is used to detect when the live view flips between
|
||||||
|
// foreground and background (in preload mode).
|
||||||
|
protected _intersectionObserver: IntersectionObserver;
|
||||||
|
|
||||||
|
// Whether or not to allow updates.
|
||||||
|
protected _messageReceived = false;
|
||||||
|
|
||||||
|
// MediaLoadedInfo object and target from the underlying live media. In the
|
||||||
|
// case of pre-loading these may be propagated later (from the original
|
||||||
|
// source).
|
||||||
|
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
||||||
|
|
||||||
|
protected _renderEpoch = 0;
|
||||||
|
|
||||||
|
constructor(host: LiveControllerHost) {
|
||||||
|
this._host = host;
|
||||||
|
this._intersectionObserver = new IntersectionObserver(
|
||||||
|
this._intersectionHandler.bind(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldUpdate(): boolean {
|
||||||
|
// Don't process updates if it's in the background and a message was
|
||||||
|
// received (otherwise an error message thrown by the background live
|
||||||
|
// component may continually be re-spammed hitting performance).
|
||||||
|
return !this._inBackground || !this._messageReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostConnected(): void {
|
||||||
|
this._intersectionObserver.observe(this._host);
|
||||||
|
|
||||||
|
this._host.addEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
||||||
|
this._host.addEventListener('frigate-card:message', this._handleMessage);
|
||||||
|
this._host.addEventListener('frigate-card:view:change', this._handleViewChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostDisconnected(): void {
|
||||||
|
this._intersectionObserver.disconnect();
|
||||||
|
|
||||||
|
this._host.removeEventListener('frigate-card:media:loaded', this._handleMediaLoaded);
|
||||||
|
this._host.removeEventListener('frigate-card:message', this._handleMessage);
|
||||||
|
this._host.removeEventListener('frigate-card:view:change', this._handleViewChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearMessageReceived(): void {
|
||||||
|
this._messageReceived = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInBackground(): boolean {
|
||||||
|
return this._inBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRenderEpoch(): number {
|
||||||
|
return this._renderEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleMessage = (ev: CustomEvent<Message>): void => {
|
||||||
|
this._messageReceived = true;
|
||||||
|
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
// Force the whole DOM to re-render next time.
|
||||||
|
this._renderEpoch++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected _handleMediaLoaded = (ev: CustomEvent<MediaLoadedInfo>): void => {
|
||||||
|
this._lastMediaLoadedInfo = {
|
||||||
|
source: ev.composedPath()[0],
|
||||||
|
mediaLoadedInfo: ev.detail,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected _handleViewChange = (ev: CustomEvent<View>): void => {
|
||||||
|
if (this._inBackground) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||||
|
const wasInBackground = this._inBackground;
|
||||||
|
this._inBackground = !entries.some((entry) => entry.isIntersecting);
|
||||||
|
|
||||||
|
if (!this._inBackground && !this._messageReceived && this._lastMediaLoadedInfo) {
|
||||||
|
// If this isn't being rendered in the background, the last render did not
|
||||||
|
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
||||||
|
dispatchExistingMediaLoadedInfoAsEvent(
|
||||||
|
// Specifically dispatch the event "where it came from", as otherwise
|
||||||
|
// the intermediate layers (e.g. media-carousel which controls the title
|
||||||
|
// popups) will not re-receive the events.
|
||||||
|
this._lastMediaLoadedInfo.source,
|
||||||
|
this._lastMediaLoadedInfo.mediaLoadedInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasInBackground !== this._inBackground) {
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch thumbnail media when a target is not already specified in the view
|
||||||
|
* (e.g. first time live is visited).
|
||||||
|
*/
|
||||||
|
public async fetchMediaInBackgroundIfNecessary(
|
||||||
|
view: View,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
cardWideConfig: CardWideConfig,
|
||||||
|
overriddenLiveConfig: LiveConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
this._inBackground ||
|
||||||
|
// Only fetch media if there isn't any already.
|
||||||
|
view.query ||
|
||||||
|
overriddenLiveConfig.controls.thumbnails.mode === 'none' ||
|
||||||
|
view.context?.live?.fetchThumbnails === false
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaType = overriddenLiveConfig.controls.thumbnails.media_type;
|
||||||
|
const now = new Date();
|
||||||
|
const viewContext: ViewContext = {
|
||||||
|
// Force the window to start at the most recent time, not
|
||||||
|
// necessarily when the most recent event/recording was:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1301
|
||||||
|
timeline: {
|
||||||
|
window: {
|
||||||
|
start: sub(now, {
|
||||||
|
seconds: overriddenLiveConfig.controls.timeline.window_seconds,
|
||||||
|
}),
|
||||||
|
end: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||||
|
if (mediaType === 'events') {
|
||||||
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
|
this._host,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
view,
|
||||||
|
{
|
||||||
|
allCameras: view.isGrid(),
|
||||||
|
targetView: view.view,
|
||||||
|
eventsMediaType: overriddenLiveConfig.controls.thumbnails.events_media_type,
|
||||||
|
select: 'latest',
|
||||||
|
// Force the window to start at the most recent time, not
|
||||||
|
// necessarily when the most recent event was:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1301
|
||||||
|
viewContext: viewContext,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (mediaType === 'recordings') {
|
||||||
|
await changeViewToRecentRecordingForCameraAndDependents(
|
||||||
|
this._host,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
view,
|
||||||
|
{
|
||||||
|
allCameras: view.isGrid(),
|
||||||
|
targetView: view.view,
|
||||||
|
select: 'latest',
|
||||||
|
viewContext: viewContext,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
} from '../utils/basic';
|
} from '../utils/basic';
|
||||||
import {
|
import {
|
||||||
FrigateMediaLoadedEventTarget,
|
FrigateCardMediaLoadedEventTarget,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaUnloadedEvent,
|
dispatchMediaUnloadedEvent,
|
||||||
} from '../utils/media-info';
|
} from '../utils/media-info';
|
||||||
@@ -23,7 +23,7 @@ const MEDIA_GRID_DEFAULT_IDEAL_CELL_WIDTH = 600;
|
|||||||
const MEDIA_GRID_DEFAULT_SELECTED_WIDTH_FACTOR = 2.0;
|
const MEDIA_GRID_DEFAULT_SELECTED_WIDTH_FACTOR = 2.0;
|
||||||
|
|
||||||
type GridID = string;
|
type GridID = string;
|
||||||
type MediaGridChild = HTMLElement & FrigateMediaLoadedEventTarget;
|
type MediaGridChild = HTMLElement & FrigateCardMediaLoadedEventTarget;
|
||||||
type MediaGridContents = Map<GridID, MediaGridChild>;
|
type MediaGridContents = Map<GridID, MediaGridChild>;
|
||||||
|
|
||||||
export interface MediaGridSelected {
|
export interface MediaGridSelected {
|
||||||
|
|||||||
+29
-122
@@ -19,6 +19,7 @@ import {
|
|||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
} from '../../card-controller/conditions-manager.js';
|
} from '../../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
||||||
|
import { LiveController } from '../../components-lib/live/live-controller.js';
|
||||||
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
@@ -34,12 +35,7 @@ import basicBlockStyle from '../../scss/basic-block.scss';
|
|||||||
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||||
import liveGridStyle from '../../scss/live-grid.scss';
|
import liveGridStyle from '../../scss/live-grid.scss';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import {
|
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
ExtendedHomeAssistant,
|
|
||||||
FrigateCardMediaPlayer,
|
|
||||||
MediaLoadedInfo,
|
|
||||||
Message,
|
|
||||||
} from '../../types.js';
|
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { aspectRatioToString, contentsChanged } from '../../utils/basic.js';
|
import { aspectRatioToString, contentsChanged } from '../../utils/basic.js';
|
||||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||||
@@ -48,10 +44,7 @@ import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/a
|
|||||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||||
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
import { getStateObjOrDispatchError } from '../../utils/get-state-obj.js';
|
||||||
import {
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
|
||||||
dispatchMediaUnloadedEvent,
|
|
||||||
} from '../../utils/media-info.js';
|
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
||||||
import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
import { dispatchViewContextChangeEvent, View } from '../../view/view.js';
|
||||||
@@ -67,24 +60,6 @@ import {
|
|||||||
getDefaultTitleConfigForView,
|
getDefaultTitleConfigForView,
|
||||||
} from '../title-control.js';
|
} from '../title-control.js';
|
||||||
|
|
||||||
interface LiveViewContext {
|
|
||||||
// A cameraID override (used for dependencies/substreams to force a different
|
|
||||||
// camera to be live rather than the camera selected in the view).
|
|
||||||
overrides?: Map<string, string>;
|
|
||||||
ptzVisible?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'view' {
|
|
||||||
interface ViewContext {
|
|
||||||
live?: LiveViewContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LastMediaLoadedInfo {
|
|
||||||
mediaLoadedInfo: MediaLoadedInfo;
|
|
||||||
source: EventTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
|
||||||
|
|
||||||
@customElement('frigate-card-live')
|
@customElement('frigate-card-live')
|
||||||
@@ -101,7 +76,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public nonOverriddenLiveConfig?: LiveConfig;
|
public nonOverriddenLiveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false }>)
|
||||||
public overriddenLiveConfig?: LiveConfig;
|
public overriddenLiveConfig?: LiveConfig;
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
@@ -119,76 +94,32 @@ export class FrigateCardLive extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public triggeredCameraIDs?: Set<string>;
|
public triggeredCameraIDs?: Set<string>;
|
||||||
|
|
||||||
// Whether or not the live view is currently in the background (i.e. preloaded
|
protected _controller = new LiveController(this);
|
||||||
// but not visible)
|
|
||||||
@state()
|
|
||||||
protected _inBackground?: boolean = false;
|
|
||||||
|
|
||||||
// Intersection handler is used to detect when the live view flips between
|
|
||||||
// foreground and background (in preload mode).
|
|
||||||
protected _intersectionObserver: IntersectionObserver;
|
|
||||||
|
|
||||||
// MediaLoadedInfo object and target from the underlying live object. In the
|
|
||||||
// case of pre-loading these may be propagated later (from the original
|
|
||||||
// source).
|
|
||||||
protected _lastMediaLoadedInfo: LastMediaLoadedInfo | null = null;
|
|
||||||
|
|
||||||
protected _messageReceivedPostRender = false;
|
|
||||||
protected _renderKey = 0;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._intersectionObserver = new IntersectionObserver(
|
|
||||||
this._intersectionHandler.bind(this),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the live view intersects with the viewport.
|
|
||||||
* @param entries The IntersectionObserverEntry entries (should be only 1).
|
|
||||||
*/
|
|
||||||
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
|
||||||
this._inBackground = !entries.some((entry) => entry.isIntersecting);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this._inBackground &&
|
|
||||||
!this._messageReceivedPostRender &&
|
|
||||||
this._lastMediaLoadedInfo
|
|
||||||
) {
|
|
||||||
// If this isn't being rendered in the background, the last render did not
|
|
||||||
// generate a message and there's a saved MediaInfo, dispatch it upwards.
|
|
||||||
dispatchExistingMediaLoadedInfoAsEvent(
|
|
||||||
// Specifically dispatch the event "where it came from", as otherwise
|
|
||||||
// the intermediate layers (e.g. media-carousel which controls the title
|
|
||||||
// popups) will not re-receive the events.
|
|
||||||
this._lastMediaLoadedInfo.source,
|
|
||||||
this._lastMediaLoadedInfo.mediaLoadedInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger a re-render which may be necessary if the prior render resulted
|
|
||||||
// in a message.
|
|
||||||
if (this._messageReceivedPostRender && !this._inBackground) {
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
||||||
// Don't process updates if it's in the background and a message was
|
return this._controller.shouldUpdate();
|
||||||
// received (otherwise an error message thrown by the background live
|
|
||||||
// component may continually be re-spammed hitting performance).
|
|
||||||
return !this._inBackground || !this._messageReceivedPostRender;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
this._intersectionObserver.observe(this);
|
if (
|
||||||
super.connectedCallback();
|
['view', 'cameraManager', 'cardWideConfig', 'overriddenLiveConfig'].some((prop) =>
|
||||||
}
|
changedProperties.has(prop),
|
||||||
|
) &&
|
||||||
|
this.view &&
|
||||||
|
this.cameraManager &&
|
||||||
|
this.cardWideConfig &&
|
||||||
|
this.overriddenLiveConfig
|
||||||
|
) {
|
||||||
|
this._controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
this.view,
|
||||||
|
this.cameraManager,
|
||||||
|
this.cardWideConfig,
|
||||||
|
this.overriddenLiveConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
this._controller.clearMessageReceived();
|
||||||
super.disconnectedCallback();
|
|
||||||
this._intersectionObserver.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -210,49 +141,25 @@ export class FrigateCardLive extends LitElement {
|
|||||||
// - The entire returned template is keyed to allow for the whole template
|
// - The entire returned template is keyed to allow for the whole template
|
||||||
// to be re-rendered in certain circumstances (specifically: if a message
|
// to be re-rendered in certain circumstances (specifically: if a message
|
||||||
// is received when the card is in the background).
|
// is received when the card is in the background).
|
||||||
const result = html`${keyed(
|
return html`${keyed(
|
||||||
this._renderKey,
|
this._controller.getRenderEpoch(),
|
||||||
html`
|
html`
|
||||||
<frigate-card-live-grid
|
<frigate-card-live-grid
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||||
.inBackground=${this._inBackground}
|
.inBackground=${this._controller.isInBackground()}
|
||||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.liveOverrides=${this.liveOverrides}
|
.liveOverrides=${this.liveOverrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.microphoneManager=${this.microphoneManager}
|
.microphoneManager=${this.microphoneManager}
|
||||||
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
||||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
|
||||||
this._renderKey++;
|
|
||||||
this._messageReceivedPostRender = true;
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
|
||||||
this._lastMediaLoadedInfo = {
|
|
||||||
source: ev.composedPath()[0],
|
|
||||||
mediaLoadedInfo: ev.detail,
|
|
||||||
};
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@frigate-card:view:change=${(ev: CustomEvent<View>) => {
|
|
||||||
if (this._inBackground) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
</frigate-card-live-grid>
|
</frigate-card-live-grid>
|
||||||
`,
|
`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
this._messageReceivedPostRender = false;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -525,7 +432,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
})
|
})
|
||||||
// Don't yet fetch thumbnails (they will be fetched when the carousel
|
// Don't yet fetch thumbnails (they will be fetched when the carousel
|
||||||
// settles).
|
// settles).
|
||||||
.mergeInContext({ thumbnails: { fetch: false } })
|
.mergeInContext({ live: { fetchThumbnails: false } })
|
||||||
.dispatchChangeEvent(this);
|
.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,7 +580,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
|
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
|
||||||
@frigate-card:carousel:settle=${() => {
|
@frigate-card:carousel:settle=${() => {
|
||||||
// Fetch the thumbnails after the carousel has settled.
|
// Fetch the thumbnails after the carousel has settled.
|
||||||
dispatchViewContextChangeEvent(this, { thumbnails: { fetch: true } });
|
dispatchViewContextChangeEvent(this, { live: { fetchThumbnails: true } });
|
||||||
}}
|
}}
|
||||||
@frigate-card:media:loaded=${() => {
|
@frigate-card:media:loaded=${() => {
|
||||||
if (this._refTitleControl.value) {
|
if (this._refTitleControl.value) {
|
||||||
|
|||||||
@@ -200,6 +200,36 @@ export function dispatchFrigateCardErrorEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Facilitates correct typing of event handlers.
|
||||||
|
export interface FrigateCardMessageEventTarget extends EventTarget {
|
||||||
|
addEventListener(
|
||||||
|
event: 'frigate-card:message',
|
||||||
|
listener: (
|
||||||
|
this: FrigateCardMessageEventTarget,
|
||||||
|
ev: CustomEvent<Message>,
|
||||||
|
) => void,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
event: 'frigate-card:message',
|
||||||
|
listener: (
|
||||||
|
this: FrigateCardMessageEventTarget,
|
||||||
|
ev: CustomEvent<Message>,
|
||||||
|
) => void,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
|
'frigate-card-progress-indicator': FrigateCardProgressIndicator;
|
||||||
|
|||||||
@@ -14,31 +14,12 @@ import {
|
|||||||
ThumbnailsControlConfig,
|
ThumbnailsControlConfig,
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
import {
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
ClipsOrSnapshotsOrAll,
|
|
||||||
EventsOrRecordings,
|
|
||||||
ExtendedHomeAssistant,
|
|
||||||
} from '../types.js';
|
|
||||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import {
|
|
||||||
changeViewToRecentEventsForCameraAndDependents,
|
|
||||||
changeViewToRecentRecordingForCameraAndDependents,
|
|
||||||
} from '../utils/media-to-view';
|
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './surround-basic.js';
|
import './surround-basic.js';
|
||||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||||
|
|
||||||
interface ThumbnailViewContext {
|
|
||||||
// Whether or not to fetch thumbnails.
|
|
||||||
fetch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'view' {
|
|
||||||
interface ViewContext {
|
|
||||||
thumbnails?: ThumbnailViewContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('frigate-card-surround')
|
@customElement('frigate-card-surround')
|
||||||
export class FrigateCardSurround extends LitElement {
|
export class FrigateCardSurround extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -53,13 +34,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public timelineConfig?: MiniTimelineControlConfig;
|
public timelineConfig?: MiniTimelineControlConfig;
|
||||||
|
|
||||||
// If fetchMedia is not specified, no fetching is done.
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public fetchMediaType?: EventsOrRecordings;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
|
||||||
public fetchEventsMediaType?: ClipsOrSnapshotsOrAll;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -68,56 +42,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
|
|
||||||
protected _cameraIDsForTimeline?: Set<string>;
|
protected _cameraIDsForTimeline?: Set<string>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch thumbnail media when a target is not specified in the view (e.g. for
|
|
||||||
* the live view).
|
|
||||||
* @param param Task parameters.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
protected async _fetchMedia(): Promise<void> {
|
|
||||||
if (
|
|
||||||
!this.cameraManager ||
|
|
||||||
!this.cardWideConfig ||
|
|
||||||
!this.fetchMediaType ||
|
|
||||||
!this.fetchEventsMediaType ||
|
|
||||||
!this.hass ||
|
|
||||||
!this.view ||
|
|
||||||
this.view.query ||
|
|
||||||
!this.thumbnailConfig ||
|
|
||||||
this.thumbnailConfig.mode === 'none' ||
|
|
||||||
!(this.view.context?.thumbnails?.fetch ?? true)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.fetchMediaType === 'events') {
|
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
|
||||||
this,
|
|
||||||
this.cameraManager,
|
|
||||||
this.cardWideConfig,
|
|
||||||
this.view,
|
|
||||||
{
|
|
||||||
allCameras: this.view.isGrid(),
|
|
||||||
targetView: this.view.view,
|
|
||||||
eventsMediaType: this.fetchEventsMediaType,
|
|
||||||
select: 'latest',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (this.fetchMediaType === 'recordings') {
|
|
||||||
await changeViewToRecentRecordingForCameraAndDependents(
|
|
||||||
this,
|
|
||||||
this.cameraManager,
|
|
||||||
this.cardWideConfig,
|
|
||||||
this.view,
|
|
||||||
{
|
|
||||||
allCameras: this.view.isGrid(),
|
|
||||||
targetView: this.view.view,
|
|
||||||
select: 'latest',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a drawer is being used.
|
* Determine if a drawer is being used.
|
||||||
* @returns `true` if a drawer is used, `false` otherwise.
|
* @returns `true` if a drawer is used, `false` otherwise.
|
||||||
@@ -128,9 +52,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before each update.
|
|
||||||
*/
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') {
|
if (this.timelineConfig?.mode && this.timelineConfig.mode !== 'none') {
|
||||||
import('./timeline-core.js');
|
import('./timeline-core.js');
|
||||||
@@ -147,15 +68,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
) {
|
) {
|
||||||
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
|
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once the component will certainly update, dispatch a media request. Only
|
|
||||||
// do so if properties relevant to the request have changed (as per their
|
|
||||||
// hasChanged).
|
|
||||||
if (
|
|
||||||
['view', 'fetch', 'browseMediaParams'].some((prop) => changedProperties.has(prop))
|
|
||||||
) {
|
|
||||||
this._fetchMedia();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDsForTimeline(): Set<string> | null {
|
protected _getCameraIDsForTimeline(): Set<string> | null {
|
||||||
|
|||||||
@@ -651,7 +651,14 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
this._removeTargetBar();
|
this._removeTargetBar();
|
||||||
|
|
||||||
if (!this._timeline || !this.view) {
|
if (
|
||||||
|
!this._timeline ||
|
||||||
|
!this.view ||
|
||||||
|
// When in mini mode, something else is in charge of the primary media
|
||||||
|
// population (e.g. the live view), in this case only act when the user
|
||||||
|
// themselves are interacting with the timeline.
|
||||||
|
(this.mini && !properties.byUser)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,12 +163,6 @@ export class FrigateCardViews extends LitElement {
|
|||||||
class="${classMap(overallClasses)}"
|
class="${classMap(overallClasses)}"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.fetchMediaType=${this.view?.is('live')
|
|
||||||
? this.overriddenConfig.live.controls.thumbnails.media_type
|
|
||||||
: undefined}
|
|
||||||
.fetchEventsMediaType=${this.view?.is('live')
|
|
||||||
? this.overriddenConfig.live.controls.thumbnails.events_media_type
|
|
||||||
: undefined}
|
|
||||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
@@ -254,6 +248,10 @@ export class FrigateCardViews extends LitElement {
|
|||||||
: ``
|
: ``
|
||||||
}
|
}
|
||||||
</frigate-card-surround>`;
|
</frigate-card-surround>`;
|
||||||
|
|
||||||
|
// .fetchMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.media_type : undefined}
|
||||||
|
// .fetchEventsMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.events_media_type : undefined}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
export type ClipsOrSnapshots = 'clips' | 'snapshots';
|
export type ClipsOrSnapshots = 'clips' | 'snapshots';
|
||||||
export type ClipsOrSnapshotsOrAll = 'clips' | 'snapshots' | 'all';
|
export type ClipsOrSnapshotsOrAll = 'clips' | 'snapshots' | 'all';
|
||||||
export type EventsOrRecordings = 'events' | 'recordings';
|
|
||||||
|
|
||||||
export class FrigateCardError extends Error {
|
export class FrigateCardError extends Error {
|
||||||
context?: unknown;
|
context?: unknown;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { EmblaCarouselType } from 'embla-carousel';
|
import { EmblaCarouselType } from 'embla-carousel';
|
||||||
|
import { LooseOptionsType } from 'embla-carousel/components/Options';
|
||||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||||
import { MediaLoadedInfo } from '../../../../types';
|
import { MediaLoadedInfo } from '../../../../types';
|
||||||
import {
|
import {
|
||||||
|
FrigateCardMediaLoadedEventTarget,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
FrigateMediaLoadedEventTarget,
|
|
||||||
} from '../../../media-info';
|
} from '../../../media-info';
|
||||||
import { LooseOptionsType } from 'embla-carousel/components/Options';
|
|
||||||
|
|
||||||
declare module 'embla-carousel/components/Plugins' {
|
declare module 'embla-carousel/components/Plugins' {
|
||||||
interface EmblaPluginsType {
|
interface EmblaPluginsType {
|
||||||
@@ -17,7 +17,7 @@ type AutoMediaLoadedInfoType = CreatePluginType<LoosePluginType, LooseOptionsTyp
|
|||||||
|
|
||||||
function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
|
function AutoMediaLoadedInfo(): AutoMediaLoadedInfoType {
|
||||||
let emblaApi: EmblaCarouselType;
|
let emblaApi: EmblaCarouselType;
|
||||||
let slides: (HTMLElement & FrigateMediaLoadedEventTarget)[] = [];
|
let slides: (HTMLElement & FrigateCardMediaLoadedEventTarget)[] = [];
|
||||||
const mediaLoadedInfo: MediaLoadedInfo[] = [];
|
const mediaLoadedInfo: MediaLoadedInfo[] = [];
|
||||||
|
|
||||||
function init(emblaApiInstance: EmblaCarouselType): void {
|
function init(emblaApiInstance: EmblaCarouselType): void {
|
||||||
|
|||||||
@@ -112,19 +112,19 @@ export function isValidMediaLoadedInfo(info: MediaLoadedInfo): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Facilitates correct Typescript typing of media:loaded/unloaded event handlers.
|
// Facilitates correct typing of event handlers.
|
||||||
export interface FrigateMediaLoadedEventTarget extends EventTarget {
|
export interface FrigateCardMediaLoadedEventTarget extends EventTarget {
|
||||||
addEventListener(
|
addEventListener(
|
||||||
event: 'frigate-card:media:loaded',
|
event: 'frigate-card:media:loaded',
|
||||||
listener: (
|
listener: (
|
||||||
this: FrigateMediaLoadedEventTarget,
|
this: FrigateCardMediaLoadedEventTarget,
|
||||||
ev: CustomEvent<MediaLoadedInfo>,
|
ev: CustomEvent<MediaLoadedInfo>,
|
||||||
) => void,
|
) => void,
|
||||||
options?: AddEventListenerOptions | boolean,
|
options?: AddEventListenerOptions | boolean,
|
||||||
): void;
|
): void;
|
||||||
addEventListener(
|
addEventListener(
|
||||||
event: 'frigate-card:media:unloaded',
|
event: 'frigate-card:media:unloaded',
|
||||||
listener: (this: FrigateMediaLoadedEventTarget, ev: CustomEvent) => void,
|
listener: (this: FrigateCardMediaLoadedEventTarget, ev: CustomEvent) => void,
|
||||||
options?: AddEventListenerOptions | boolean,
|
options?: AddEventListenerOptions | boolean,
|
||||||
): void;
|
): void;
|
||||||
addEventListener(
|
addEventListener(
|
||||||
@@ -135,14 +135,14 @@ export interface FrigateMediaLoadedEventTarget extends EventTarget {
|
|||||||
removeEventListener(
|
removeEventListener(
|
||||||
event: 'frigate-card:media:loaded',
|
event: 'frigate-card:media:loaded',
|
||||||
listener: (
|
listener: (
|
||||||
this: FrigateMediaLoadedEventTarget,
|
this: FrigateCardMediaLoadedEventTarget,
|
||||||
ev: CustomEvent<MediaLoadedInfo>,
|
ev: CustomEvent<MediaLoadedInfo>,
|
||||||
) => void,
|
) => void,
|
||||||
options?: boolean | EventListenerOptions,
|
options?: boolean | EventListenerOptions,
|
||||||
): void;
|
): void;
|
||||||
removeEventListener(
|
removeEventListener(
|
||||||
event: 'frigate-card:media:unloaded',
|
event: 'frigate-card:media:unloaded',
|
||||||
listener: (this: FrigateMediaLoadedEventTarget, ev: CustomEvent) => void,
|
listener: (this: FrigateCardMediaLoadedEventTarget, ev: CustomEvent) => void,
|
||||||
options?: boolean | EventListenerOptions,
|
options?: boolean | EventListenerOptions,
|
||||||
): void;
|
): void;
|
||||||
removeEventListener(
|
removeEventListener(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
targetView?: FrigateCardView;
|
targetView?: FrigateCardView;
|
||||||
select?: ResultSelectType;
|
select?: ResultSelectType;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
|
viewContext?: ViewContext;
|
||||||
},
|
},
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const capabilitySearch: CapabilitySearchOptions =
|
const capabilitySearch: CapabilitySearchOptions =
|
||||||
@@ -61,6 +62,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
targetView: options?.targetView,
|
targetView: options?.targetView,
|
||||||
select: options?.select,
|
select: options?.select,
|
||||||
useCache: options?.useCache,
|
useCache: options?.useCache,
|
||||||
|
viewContext: options?.viewContext
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)?.dispatchChangeEvent(element);
|
)?.dispatchChangeEvent(element);
|
||||||
@@ -103,6 +105,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
targetView?: FrigateCardView;
|
targetView?: FrigateCardView;
|
||||||
select?: ResultSelectType;
|
select?: ResultSelectType;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
|
viewContext?: ViewContext;
|
||||||
},
|
},
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const cameraIDs = options?.allCameras
|
const cameraIDs = options?.allCameras
|
||||||
@@ -131,6 +134,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
targetView: options?.targetView,
|
targetView: options?.targetView,
|
||||||
select: options?.select,
|
select: options?.select,
|
||||||
useCache: options?.useCache,
|
useCache: options?.useCache,
|
||||||
|
viewContext: options?.viewContext,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)?.dispatchChangeEvent(element);
|
)?.dispatchChangeEvent(element);
|
||||||
@@ -159,6 +163,7 @@ export const executeMediaQueryForView = async (
|
|||||||
targetTime?: Date;
|
targetTime?: Date;
|
||||||
select?: ResultSelectType;
|
select?: ResultSelectType;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
|
viewContext?: ViewContext;
|
||||||
},
|
},
|
||||||
): Promise<View | null> => {
|
): Promise<View | null> => {
|
||||||
const queries = query.getQueries();
|
const queries = query.getQueries();
|
||||||
@@ -195,7 +200,8 @@ export const executeMediaQueryForView = async (
|
|||||||
view: options?.targetView,
|
view: options?.targetView,
|
||||||
camera: cameraID,
|
camera: cameraID,
|
||||||
})
|
})
|
||||||
.mergeInContext(viewerContext);
|
.mergeInContext(options?.viewContext)
|
||||||
|
.mergeInContext(viewerContext)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const executeMediaQueryForViewWithErrorDispatching = async (
|
export const executeMediaQueryForViewWithErrorDispatching = async (
|
||||||
@@ -209,6 +215,7 @@ export const executeMediaQueryForViewWithErrorDispatching = async (
|
|||||||
targetTime?: Date;
|
targetTime?: Date;
|
||||||
select?: ResultSelectType;
|
select?: ResultSelectType;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
|
viewContext?: ViewContext;
|
||||||
},
|
},
|
||||||
): Promise<View | null> => {
|
): Promise<View | null> => {
|
||||||
try {
|
try {
|
||||||
@@ -218,6 +225,7 @@ export const executeMediaQueryForViewWithErrorDispatching = async (
|
|||||||
targetTime: options?.targetTime,
|
targetTime: options?.targetTime,
|
||||||
select: options?.select,
|
select: options?.select,
|
||||||
useCache: options?.useCache,
|
useCache: options?.useCache,
|
||||||
|
viewContext: options?.viewContext,
|
||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
|
|||||||
@@ -269,6 +269,36 @@ export class View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Facilitates correct typing of event handlers.
|
||||||
|
export interface FrigateCardViewChangeEventTarget extends EventTarget {
|
||||||
|
addEventListener(
|
||||||
|
event: 'frigate-card:view:change',
|
||||||
|
listener: (
|
||||||
|
this: FrigateCardViewChangeEventTarget,
|
||||||
|
ev: CustomEvent<View>,
|
||||||
|
) => void,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
event: 'frigate-card:view:change',
|
||||||
|
listener: (
|
||||||
|
this: FrigateCardViewChangeEventTarget,
|
||||||
|
ev: CustomEvent<View>,
|
||||||
|
) => void,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch an event to change the view context.
|
* Dispatch an event to change the view context.
|
||||||
* @param target The EventTarget to send the event from.
|
* @param target The EventTarget to send the event from.
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
|
||||||
CardElementManager,
|
|
||||||
CardHTMLElement,
|
|
||||||
} from '../../src/card-controller/card-element-manager';
|
|
||||||
import { createCardAPI } from '../test-utils';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
||||||
const createElement = (): CardHTMLElement => {
|
import { createCardAPI, createLitElement } from '../test-utils';
|
||||||
const element = document.createElement('div') as unknown as CardHTMLElement;
|
|
||||||
element.requestUpdate = vi.fn();
|
|
||||||
return element as CardHTMLElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('CardElementManager', () => {
|
describe('CardElementManager', () => {
|
||||||
@@ -20,7 +11,7 @@ describe('CardElementManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get element', () => {
|
it('should get element', () => {
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
const manager = new CardElementManager(
|
const manager = new CardElementManager(
|
||||||
createCardAPI(),
|
createCardAPI(),
|
||||||
element,
|
element,
|
||||||
@@ -35,7 +26,7 @@ describe('CardElementManager', () => {
|
|||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
const manager = new CardElementManager(
|
const manager = new CardElementManager(
|
||||||
createCardAPI(),
|
createCardAPI(),
|
||||||
createElement(),
|
createLitElement(),
|
||||||
callback,
|
callback,
|
||||||
() => undefined,
|
() => undefined,
|
||||||
);
|
);
|
||||||
@@ -49,7 +40,7 @@ describe('CardElementManager', () => {
|
|||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
const manager = new CardElementManager(
|
const manager = new CardElementManager(
|
||||||
createCardAPI(),
|
createCardAPI(),
|
||||||
createElement(),
|
createLitElement(),
|
||||||
() => undefined,
|
() => undefined,
|
||||||
callback,
|
callback,
|
||||||
);
|
);
|
||||||
@@ -60,7 +51,7 @@ describe('CardElementManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update', () => {
|
it('should update', () => {
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
const manager = new CardElementManager(
|
const manager = new CardElementManager(
|
||||||
createCardAPI(),
|
createCardAPI(),
|
||||||
element,
|
element,
|
||||||
@@ -73,7 +64,7 @@ describe('CardElementManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get hasUpdated', () => {
|
it('should get hasUpdated', () => {
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
element.hasUpdated = true;
|
element.hasUpdated = true;
|
||||||
const manager = new CardElementManager(
|
const manager = new CardElementManager(
|
||||||
createCardAPI(),
|
createCardAPI(),
|
||||||
@@ -86,7 +77,7 @@ describe('CardElementManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get height', () => {
|
it('should get height', () => {
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
||||||
width: 200,
|
width: 200,
|
||||||
height: 800,
|
height: 800,
|
||||||
@@ -106,7 +97,7 @@ describe('CardElementManager', () => {
|
|||||||
const windowAddEventListener = vi.spyOn(global.window, 'addEventListener');
|
const windowAddEventListener = vi.spyOn(global.window, 'addEventListener');
|
||||||
|
|
||||||
const addEventListener = vi.fn();
|
const addEventListener = vi.fn();
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
element.addEventListener = addEventListener;
|
element.addEventListener = addEventListener;
|
||||||
|
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
@@ -147,7 +138,7 @@ describe('CardElementManager', () => {
|
|||||||
it('should disconnect', () => {
|
it('should disconnect', () => {
|
||||||
const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener');
|
const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener');
|
||||||
|
|
||||||
const element = createElement();
|
const element = createLitElement();
|
||||||
element.setAttribute('panel', '');
|
element.setAttribute('panel', '');
|
||||||
|
|
||||||
const removeEventListener = vi.fn();
|
const removeEventListener = vi.fn();
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('ViewManager.setView', () => {
|
|||||||
it('should set view with new context', () => {
|
it('should set view with new context', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new ViewManager(api);
|
const manager = new ViewManager(api);
|
||||||
const context = { thumbnails: { fetch: false } };
|
const context = { live: { fetchThumbnails: false } };
|
||||||
|
|
||||||
// Setting context with no existing view does nothing.
|
// Setting context with no existing view does nothing.
|
||||||
manager.setViewWithNewContext(context);
|
manager.setViewWithNewContext(context);
|
||||||
|
|||||||
@@ -0,0 +1,377 @@
|
|||||||
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { LiveController } from '../../../src/components-lib/live/live-controller';
|
||||||
|
import { dispatchMessageEvent } from '../../../src/components/message';
|
||||||
|
import {
|
||||||
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
|
changeViewToRecentRecordingForCameraAndDependents,
|
||||||
|
} from '../../../src/utils/media-to-view';
|
||||||
|
import { EventMediaQueries } from '../../../src/view/media-queries';
|
||||||
|
import {
|
||||||
|
IntersectionObserverMock,
|
||||||
|
callIntersectionHandler,
|
||||||
|
createCameraManager,
|
||||||
|
createConfig,
|
||||||
|
createLitElement,
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
createMediaLoadedInfoEvent,
|
||||||
|
createParent,
|
||||||
|
createView,
|
||||||
|
createViewChangeEvent,
|
||||||
|
} from '../../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../src/utils/media-to-view');
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('LiveController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be constructable', () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
expect(controller).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect and disconnect', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const parent = createParent({ children: [host] });
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
parent.addEventListener('frigate-card:message', eventListener);
|
||||||
|
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
|
||||||
|
expect(eventListener).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
|
||||||
|
expect(eventListener).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle background / foreground', () => {
|
||||||
|
it('should start in the foreground', () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
expect(controller.isInBackground()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle changing to background', () => {
|
||||||
|
const element = createLitElement();
|
||||||
|
const controller = new LiveController(element);
|
||||||
|
expect(controller.isInBackground()).toBeFalsy();
|
||||||
|
expect(element.requestUpdate).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
expect(controller.isInBackground()).toBeFalsy();
|
||||||
|
expect(element.requestUpdate).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
expect(controller.isInBackground()).toBeTruthy();
|
||||||
|
expect(element.requestUpdate).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch media loaded on background change', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const parent = createParent({ children: [host] });
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
parent.addEventListener('frigate-card:media:loaded', eventListener);
|
||||||
|
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
const mediaLoadedInfo = createMediaLoadedInfo();
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
expect(controller.isInBackground()).toBeTruthy();
|
||||||
|
|
||||||
|
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
||||||
|
expect(eventListener).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
expect(eventListener).toBeCalledTimes(1);
|
||||||
|
expect(eventListener).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
detail: mediaLoadedInfo,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
host.dispatchEvent(createMediaLoadedInfoEvent(mediaLoadedInfo));
|
||||||
|
expect(eventListener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
expect(eventListener).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
controller.clearMessageReceived();
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
expect(eventListener).toBeCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should correctly allow updates', () => {
|
||||||
|
it('when not in background', () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
expect(controller.shouldUpdate()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when in background without message', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
|
||||||
|
expect(controller.shouldUpdate()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when in background with message', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
|
||||||
|
expect(controller.shouldUpdate()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle message', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const parent = createParent({ children: [host] });
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
parent.addEventListener('frigate-card:message', eventListener);
|
||||||
|
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
controller.hostConnected();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
expect(controller.isInBackground()).toBeTruthy();
|
||||||
|
|
||||||
|
const firstRenderEpoch = controller.getRenderEpoch();
|
||||||
|
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
expect(eventListener).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
const secondRenderEpoch = controller.getRenderEpoch();
|
||||||
|
expect(secondRenderEpoch).not.toBe(firstRenderEpoch);
|
||||||
|
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
|
||||||
|
dispatchMessageEvent(host, 'message', 'info');
|
||||||
|
expect(eventListener).toBeCalledTimes(1);
|
||||||
|
expect(controller.getRenderEpoch()).toBe(secondRenderEpoch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle view change', () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const parent = createParent({ children: [host] });
|
||||||
|
const eventListener = vi.fn();
|
||||||
|
parent.addEventListener('frigate-card:view:change', eventListener);
|
||||||
|
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
controller.hostConnected();
|
||||||
|
const view = createView();
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
expect(controller.isInBackground()).toBeTruthy();
|
||||||
|
host.dispatchEvent(createViewChangeEvent(view));
|
||||||
|
|
||||||
|
expect(eventListener).toBeCalledTimes(0);
|
||||||
|
|
||||||
|
callIntersectionHandler(true);
|
||||||
|
expect(controller.isInBackground()).toBeFalsy();
|
||||||
|
|
||||||
|
host.dispatchEvent(createViewChangeEvent(view));
|
||||||
|
|
||||||
|
expect(eventListener).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should fetch media', () => {
|
||||||
|
it('when in background', async () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
|
||||||
|
callIntersectionHandler(false);
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
createView(),
|
||||||
|
createCameraManager(),
|
||||||
|
{},
|
||||||
|
createConfig().live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when has existing query', async () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
createView({ query: new EventMediaQueries() }),
|
||||||
|
createCameraManager(),
|
||||||
|
{},
|
||||||
|
createConfig().live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when has no thumbnails', async () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
createView(),
|
||||||
|
createCameraManager(),
|
||||||
|
{},
|
||||||
|
createConfig({
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
mode: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when fetch disabled in context', async () => {
|
||||||
|
const controller = new LiveController(createLitElement());
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
createView({
|
||||||
|
context: {
|
||||||
|
live: {
|
||||||
|
fetchThumbnails: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createCameraManager(),
|
||||||
|
{},
|
||||||
|
createConfig().live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with fetch', () => {
|
||||||
|
const now = new Date('2024-04-07T19:43');
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('events', async () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
const view = createView();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
const cardWideConfig = {};
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
view,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
createConfig({
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
media_type: 'events',
|
||||||
|
events_media_type: 'all',
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
window_seconds: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).toBeCalledWith(
|
||||||
|
host,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
view,
|
||||||
|
expect.objectContaining({
|
||||||
|
allCameras: false,
|
||||||
|
targetView: 'live',
|
||||||
|
eventsMediaType: 'all',
|
||||||
|
select: 'latest',
|
||||||
|
viewContext: expect.objectContaining({
|
||||||
|
timeline: {
|
||||||
|
window: {
|
||||||
|
start: new Date('2024-04-07T18:43'),
|
||||||
|
end: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recordings', async () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const controller = new LiveController(host);
|
||||||
|
const view = createView();
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
const cardWideConfig = {};
|
||||||
|
|
||||||
|
await controller.fetchMediaInBackgroundIfNecessary(
|
||||||
|
view,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
createConfig({
|
||||||
|
live: {
|
||||||
|
controls: {
|
||||||
|
thumbnails: {
|
||||||
|
media_type: 'recordings',
|
||||||
|
},
|
||||||
|
timeline: {
|
||||||
|
window_seconds: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).live,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(changeViewToRecentEventsForCameraAndDependents).not.toBeCalled();
|
||||||
|
expect(changeViewToRecentRecordingForCameraAndDependents).toBeCalledWith(
|
||||||
|
host,
|
||||||
|
cameraManager,
|
||||||
|
cardWideConfig,
|
||||||
|
view,
|
||||||
|
expect.objectContaining({
|
||||||
|
allCameras: false,
|
||||||
|
targetView: 'live',
|
||||||
|
select: 'latest',
|
||||||
|
viewContext: expect.objectContaining({
|
||||||
|
timeline: {
|
||||||
|
window: {
|
||||||
|
start: new Date('2024-04-07T18:43'),
|
||||||
|
end: now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import endOfDay from 'date-fns/endOfDay';
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
import sub from 'date-fns/sub';
|
import sub from 'date-fns/sub';
|
||||||
import { LitElement } from 'lit';
|
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||||
import { CameraManagerStore } from '../../src/camera-manager/store';
|
import { CameraManagerStore } from '../../src/camera-manager/store';
|
||||||
@@ -23,6 +22,7 @@ import {
|
|||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCapabilities,
|
createCapabilities,
|
||||||
|
createLitElement,
|
||||||
createPerformanceConfig,
|
createPerformanceConfig,
|
||||||
createStore,
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
@@ -30,12 +30,6 @@ import {
|
|||||||
|
|
||||||
vi.mock('../../src/utils/media-to-view');
|
vi.mock('../../src/utils/media-to-view');
|
||||||
|
|
||||||
const createHost = (): LitElement => {
|
|
||||||
const host = document.createElement('div') as unknown as LitElement;
|
|
||||||
host.requestUpdate = vi.fn();
|
|
||||||
return host;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createCameraStore = (options?: {
|
const createCameraStore = (options?: {
|
||||||
capabilities: Capabilities;
|
capabilities: Capabilities;
|
||||||
}): CameraManagerStore => {
|
}): CameraManagerStore => {
|
||||||
@@ -64,7 +58,7 @@ describe('MediaFilterController', () => {
|
|||||||
|
|
||||||
describe('should have correct default options', () => {
|
describe('should have correct default options', () => {
|
||||||
it('media type', () => {
|
it('media type', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getMediaTypeOptions()).toEqual([
|
expect(controller.getMediaTypeOptions()).toEqual([
|
||||||
{
|
{
|
||||||
value: MediaFilterMediaType.Clips,
|
value: MediaFilterMediaType.Clips,
|
||||||
@@ -82,7 +76,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('favorite', () => {
|
it('favorite', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getFavoriteOptions()).toEqual([
|
expect(controller.getFavoriteOptions()).toEqual([
|
||||||
{
|
{
|
||||||
value: MediaFilterCoreFavoriteSelection.Favorite,
|
value: MediaFilterCoreFavoriteSelection.Favorite,
|
||||||
@@ -96,7 +90,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('when', () => {
|
it('when', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getWhenOptions()).toEqual([
|
expect(controller.getWhenOptions()).toEqual([
|
||||||
{
|
{
|
||||||
value: MediaFilterCoreWhen.Today,
|
value: MediaFilterCoreWhen.Today,
|
||||||
@@ -122,22 +116,22 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('cameras', () => {
|
it('cameras', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getCameraOptions()).toEqual([]);
|
expect(controller.getCameraOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('what', () => {
|
it('what', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getWhatOptions()).toEqual([]);
|
expect(controller.getWhatOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('where', () => {
|
it('where', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getWhereOptions()).toEqual([]);
|
expect(controller.getWhereOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tags', () => {
|
it('tags', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getTagsOptions()).toEqual([]);
|
expect(controller.getTagsOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -152,7 +146,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
controller.computeCameraOptions(cameraManager);
|
controller.computeCameraOptions(cameraManager);
|
||||||
expect(controller.getCameraOptions()).toEqual([
|
expect(controller.getCameraOptions()).toEqual([
|
||||||
{
|
{
|
||||||
@@ -178,7 +172,7 @@ describe('MediaFilterController', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
controller.computeCameraOptions(cameraManager);
|
controller.computeCameraOptions(cameraManager);
|
||||||
expect(controller.getCameraOptions()).toEqual([]);
|
expect(controller.getCameraOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -188,7 +182,7 @@ describe('MediaFilterController', () => {
|
|||||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue(null);
|
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue(null);
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
controller.computeCameraOptions(cameraManager);
|
controller.computeCameraOptions(cameraManager);
|
||||||
expect(controller.getCameraOptions()).toEqual([
|
expect(controller.getCameraOptions()).toEqual([
|
||||||
{
|
{
|
||||||
@@ -206,7 +200,7 @@ describe('MediaFilterController', () => {
|
|||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getMediaMetadata).mockRejectedValue(new Error('error'));
|
vi.mocked(cameraManager.getMediaMetadata).mockRejectedValue(new Error('error'));
|
||||||
|
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.computeMetadataOptions(cameraManager);
|
await controller.computeMetadataOptions(cameraManager);
|
||||||
expect(host.requestUpdate).not.toBeCalled();
|
expect(host.requestUpdate).not.toBeCalled();
|
||||||
@@ -218,7 +212,7 @@ describe('MediaFilterController', () => {
|
|||||||
what: new Set(['person', 'car']),
|
what: new Set(['person', 'car']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.computeMetadataOptions(cameraManager);
|
await controller.computeMetadataOptions(cameraManager);
|
||||||
expect(controller.getWhatOptions()).toEqual([
|
expect(controller.getWhatOptions()).toEqual([
|
||||||
@@ -240,7 +234,7 @@ describe('MediaFilterController', () => {
|
|||||||
where: new Set(['front_door', 'back_yard']),
|
where: new Set(['front_door', 'back_yard']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.computeMetadataOptions(cameraManager);
|
await controller.computeMetadataOptions(cameraManager);
|
||||||
expect(controller.getWhereOptions()).toEqual([
|
expect(controller.getWhereOptions()).toEqual([
|
||||||
@@ -262,7 +256,7 @@ describe('MediaFilterController', () => {
|
|||||||
tags: new Set(['tag-1', 'tag-2']),
|
tags: new Set(['tag-1', 'tag-2']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.computeMetadataOptions(cameraManager);
|
await controller.computeMetadataOptions(cameraManager);
|
||||||
expect(controller.getTagsOptions()).toEqual([
|
expect(controller.getTagsOptions()).toEqual([
|
||||||
@@ -284,7 +278,7 @@ describe('MediaFilterController', () => {
|
|||||||
days: new Set(['2024-02-04', '2024-02-05']),
|
days: new Set(['2024-02-04', '2024-02-05']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.computeMetadataOptions(cameraManager);
|
await controller.computeMetadataOptions(cameraManager);
|
||||||
|
|
||||||
@@ -306,7 +300,7 @@ describe('MediaFilterController', () => {
|
|||||||
const view = createView({ query: new EventMediaQueries() });
|
const view = createView({ query: new EventMediaQueries() });
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||||
events: true,
|
events: true,
|
||||||
recordings: false,
|
recordings: false,
|
||||||
@@ -317,7 +311,7 @@ describe('MediaFilterController', () => {
|
|||||||
const view = createView({ query: new RecordingMediaQueries() });
|
const view = createView({ query: new RecordingMediaQueries() });
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||||
events: false,
|
events: false,
|
||||||
recordings: true,
|
recordings: true,
|
||||||
@@ -331,7 +325,7 @@ describe('MediaFilterController', () => {
|
|||||||
createCapabilities({ 'favorite-events': true }),
|
createCapabilities({ 'favorite-events': true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||||
favorites: true,
|
favorites: true,
|
||||||
});
|
});
|
||||||
@@ -344,7 +338,7 @@ describe('MediaFilterController', () => {
|
|||||||
createCapabilities({ 'favorite-recordings': true }),
|
createCapabilities({ 'favorite-recordings': true }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||||
favorites: true,
|
favorites: true,
|
||||||
});
|
});
|
||||||
@@ -354,7 +348,7 @@ describe('MediaFilterController', () => {
|
|||||||
const view = createView();
|
const view = createView();
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
|
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
expect(controller.getControlsToShow(cameraManager, view)).toMatchObject({
|
||||||
favorites: false,
|
favorites: false,
|
||||||
});
|
});
|
||||||
@@ -363,7 +357,7 @@ describe('MediaFilterController', () => {
|
|||||||
|
|
||||||
describe('should handle value change', () => {
|
describe('should handle value change', () => {
|
||||||
it('must have visible cameras', async () => {
|
it('must have visible cameras', async () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
await controller.valueChangeHandler(
|
await controller.valueChangeHandler(
|
||||||
createCameraManager(),
|
createCameraManager(),
|
||||||
@@ -380,7 +374,7 @@ describe('MediaFilterController', () => {
|
|||||||
'%s',
|
'%s',
|
||||||
async (viewName: 'clips' | 'snapshots') => {
|
async (viewName: 'clips' | 'snapshots') => {
|
||||||
const eventListener = vi.fn();
|
const eventListener = vi.fn();
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
host.addEventListener('frigate-card:view:change', eventListener);
|
host.addEventListener('frigate-card:view:change', eventListener);
|
||||||
|
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
@@ -458,7 +452,7 @@ describe('MediaFilterController', () => {
|
|||||||
|
|
||||||
it('with recordings media type', async () => {
|
it('with recordings media type', async () => {
|
||||||
const eventListener = vi.fn();
|
const eventListener = vi.fn();
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
host.addEventListener('frigate-card:view:change', eventListener);
|
host.addEventListener('frigate-card:view:change', eventListener);
|
||||||
|
|
||||||
const controller = new MediaFilterController(host);
|
const controller = new MediaFilterController(host);
|
||||||
@@ -522,7 +516,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('without favorites', async () => {
|
it('without favorites', async () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
@@ -583,7 +577,7 @@ describe('MediaFilterController', () => {
|
|||||||
new Date('2024-02-29T23:59:59.999'),
|
new Date('2024-02-29T23:59:59.999'),
|
||||||
],
|
],
|
||||||
])('%s', async (value: MediaFilterCoreWhen | string, from: Date, to: Date) => {
|
])('%s', async (value: MediaFilterCoreWhen | string, from: Date, to: Date) => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
@@ -614,7 +608,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('custom without values', async () => {
|
it('custom without values', async () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
@@ -646,7 +640,7 @@ describe('MediaFilterController', () => {
|
|||||||
|
|
||||||
describe('should calculate correct defaults', () => {
|
describe('should calculate correct defaults', () => {
|
||||||
it('with no queries', () => {
|
it('with no queries', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
|
|
||||||
controller.computeInitialDefaultsFromView(createCameraManager(), createView());
|
controller.computeInitialDefaultsFromView(createCameraManager(), createView());
|
||||||
|
|
||||||
@@ -654,7 +648,7 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with no cameras', () => {
|
it('with no cameras', () => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
|
|
||||||
controller.computeInitialDefaultsFromView(
|
controller.computeInitialDefaultsFromView(
|
||||||
createCameraManager(),
|
createCameraManager(),
|
||||||
@@ -956,7 +950,7 @@ describe('MediaFilterController', () => {
|
|||||||
mediaQueries: MediaQueries,
|
mediaQueries: MediaQueries,
|
||||||
defaults: MediaFilterCoreDefaults | null,
|
defaults: MediaFilterCoreDefaults | null,
|
||||||
) => {
|
) => {
|
||||||
const controller = new MediaFilterController(createHost());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
vi.mocked(cameraManager.getStore).mockReturnValue(createCameraStore());
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import {
|
import { HASSDomEvent, handleActionConfig } from '@dermotduffy/custom-card-helpers';
|
||||||
HASSDomEvent,
|
|
||||||
handleActionConfig,
|
|
||||||
} from '@dermotduffy/custom-card-helpers';
|
|
||||||
import { LitElement } from 'lit';
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { FRIGATE_ICON_SVG_PATH } from '../../src/camera-manager/frigate/icon';
|
import { FRIGATE_ICON_SVG_PATH } from '../../src/camera-manager/frigate/icon';
|
||||||
import { MenuController } from '../../src/components-lib/menu-controller';
|
import { MenuController } from '../../src/components-lib/menu-controller';
|
||||||
import { ActionsConfig, MenuConfig, menuConfigSchema } from '../../src/config/types';
|
import { ActionsConfig, MenuConfig, menuConfigSchema } from '../../src/config/types';
|
||||||
import { StateParameters } from '../../src/types';
|
import { StateParameters } from '../../src/types';
|
||||||
import { refreshDynamicStateParameters } from '../../src/utils/ha';
|
import { refreshDynamicStateParameters } from '../../src/utils/ha';
|
||||||
import { createHASS } from '../test-utils';
|
import { createHASS, createLitElement } from '../test-utils';
|
||||||
|
|
||||||
vi.mock('@dermotduffy/custom-card-helpers');
|
vi.mock('@dermotduffy/custom-card-helpers');
|
||||||
vi.mock('../../src/utils/ha');
|
vi.mock('../../src/utils/ha');
|
||||||
|
|
||||||
const createHost = (): LitElement => {
|
|
||||||
const host = document.createElement('div') as unknown as LitElement;
|
|
||||||
host.requestUpdate = vi.fn();
|
|
||||||
return host;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createMenuConfig = (config: unknown): MenuConfig => {
|
const createMenuConfig = (config: unknown): MenuConfig => {
|
||||||
return menuConfigSchema.parse(config);
|
return menuConfigSchema.parse(config);
|
||||||
};
|
};
|
||||||
@@ -59,7 +49,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set and get menu config', () => {
|
it('should set and get menu config', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
|
|
||||||
const config = createMenuConfig({
|
const config = createMenuConfig({
|
||||||
@@ -79,7 +69,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should set and sort buttons', () => {
|
describe('should set and sort buttons', () => {
|
||||||
it('by priority', () => {
|
it('by priority', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.setButtons([
|
controller.setButtons([
|
||||||
{
|
{
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
@@ -146,7 +136,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with frigate button first', () => {
|
it('with frigate button first', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
createMenuConfig({
|
createMenuConfig({
|
||||||
style: 'hidden',
|
style: 'hidden',
|
||||||
@@ -197,7 +187,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should get buttons', () => {
|
describe('should get buttons', () => {
|
||||||
it('with matching alignment', () => {
|
it('with matching alignment', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.setButtons([
|
controller.setButtons([
|
||||||
{
|
{
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
@@ -229,7 +219,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with disabled buttons', () => {
|
it('with disabled buttons', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.setButtons([
|
controller.setButtons([
|
||||||
{
|
{
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
@@ -261,7 +251,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with hidden non-expanded menu', () => {
|
it('with hidden non-expanded menu', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
createMenuConfig({
|
createMenuConfig({
|
||||||
style: 'hidden',
|
style: 'hidden',
|
||||||
@@ -303,7 +293,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should get fresh button state', () => {
|
describe('should get fresh button state', () => {
|
||||||
it('on state icon', () => {
|
it('on state icon', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
const stateButton = {
|
const stateButton = {
|
||||||
type: 'custom:frigate-card-menu-state-icon' as const,
|
type: 'custom:frigate-card-menu-state-icon' as const,
|
||||||
icon: 'mdi:sheep',
|
icon: 'mdi:sheep',
|
||||||
@@ -322,7 +312,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('on non state icon', () => {
|
it('on non state icon', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
const button = {
|
const button = {
|
||||||
type: 'custom:frigate-card-menu-icon' as const,
|
type: 'custom:frigate-card-menu-icon' as const,
|
||||||
icon: 'mdi:sheep',
|
icon: 'mdi:sheep',
|
||||||
@@ -335,7 +325,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should get svg path', () => {
|
describe('should get svg path', () => {
|
||||||
it('frigate icon', () => {
|
it('frigate icon', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
const button = {
|
const button = {
|
||||||
type: 'custom:frigate-card-menu-icon' as const,
|
type: 'custom:frigate-card-menu-icon' as const,
|
||||||
icon: 'frigate',
|
icon: 'frigate',
|
||||||
@@ -344,7 +334,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('non-frigate icon', () => {
|
it('non-frigate icon', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
const button = {
|
const button = {
|
||||||
type: 'custom:frigate-card-menu-icon' as const,
|
type: 'custom:frigate-card-menu-icon' as const,
|
||||||
icon: 'mdi:cow',
|
icon: 'mdi:cow',
|
||||||
@@ -355,13 +345,13 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should handle actions', () => {
|
describe('should handle actions', () => {
|
||||||
it('should bail without config', () => {
|
it('should bail without config', () => {
|
||||||
const controller = new MenuController(createHost());
|
const controller = new MenuController(createLitElement());
|
||||||
controller.actionHandler(createHASS(), createEvent('tap'));
|
controller.actionHandler(createHASS(), createEvent('tap'));
|
||||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute simple action in non-hidden menu', () => {
|
it('should execute simple action in non-hidden menu', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
|
|
||||||
@@ -377,7 +367,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should execute simple action in with config in event', () => {
|
it('should execute simple action in with config in event', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
|
|
||||||
@@ -391,7 +381,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should execute simple array of actions in non-hidden menu', () => {
|
it('should execute simple array of actions in non-hidden menu', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
|
|
||||||
@@ -401,7 +391,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should close menu', () => {
|
describe('should close menu', () => {
|
||||||
it('tap', () => {
|
it('tap', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
@@ -418,7 +408,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('end_tap', () => {
|
it('end_tap', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
@@ -439,7 +429,7 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should not close menu', () => {
|
describe('should not close menu', () => {
|
||||||
it('start_tap with later action', () => {
|
it('start_tap with later action', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
@@ -459,7 +449,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('with a menu toggle action', () => {
|
it('with a menu toggle action', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
@@ -479,7 +469,7 @@ describe('MenuController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('when no action is actually taken', () => {
|
it('when no action is actually taken', () => {
|
||||||
const host = createHost();
|
const host = createLitElement();
|
||||||
const hass = createHASS();
|
const hass = createHASS();
|
||||||
const controller = new MenuController(host);
|
const controller = new MenuController(host);
|
||||||
controller.setMenuConfig(
|
controller.setMenuConfig(
|
||||||
|
|||||||
+40
-2
@@ -1,5 +1,6 @@
|
|||||||
import { CurrentUser, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { CurrentUser, HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||||
|
import { LitElement } from 'lit';
|
||||||
import { expect, vi } from 'vitest';
|
import { expect, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { Camera } from '../src/camera-manager/camera';
|
import { Camera } from '../src/camera-manager/camera';
|
||||||
@@ -13,7 +14,6 @@ import {
|
|||||||
CameraEventCallback,
|
CameraEventCallback,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
} from '../src/camera-manager/types';
|
} from '../src/camera-manager/types';
|
||||||
import { CapabilitiesRaw } from '../src/types';
|
|
||||||
import { ActionsManager } from '../src/card-controller/actions-manager';
|
import { ActionsManager } from '../src/card-controller/actions-manager';
|
||||||
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
||||||
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
performanceConfigSchema,
|
performanceConfigSchema,
|
||||||
} from '../src/config/types';
|
} from '../src/config/types';
|
||||||
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
import { CapabilitiesRaw, ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||||
import { EntityRegistryManager } from '../src/utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../src/utils/ha/entity-registry';
|
||||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||||
@@ -248,6 +248,24 @@ export const createMediaLoadedInfo = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createMediaLoadedInfoEvent = (
|
||||||
|
mediaLoadedInfo?: MediaLoadedInfo,
|
||||||
|
): CustomEvent<MediaLoadedInfo> => {
|
||||||
|
return new CustomEvent('frigate-card:media:loaded', {
|
||||||
|
detail: mediaLoadedInfo ?? createMediaLoadedInfo(),
|
||||||
|
composed: true,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createViewChangeEvent = (view?: View): CustomEvent<View> => {
|
||||||
|
return new CustomEvent('frigate-card:view:change', {
|
||||||
|
detail: view ?? createView(),
|
||||||
|
composed: true,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const createPerformanceConfig = (config: unknown): PerformanceConfig => {
|
export const createPerformanceConfig = (config: unknown): PerformanceConfig => {
|
||||||
return performanceConfigSchema.parse(config);
|
return performanceConfigSchema.parse(config);
|
||||||
};
|
};
|
||||||
@@ -342,6 +360,20 @@ export const requestAnimationFrameMock = (callback: FrameRequestCallback) => {
|
|||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const callIntersectionHandler = (intersecting = true, n = 0): void => {
|
||||||
|
const mockResult = vi.mocked(IntersectionObserver).mock.results[n];
|
||||||
|
if (mockResult.type !== 'return') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = mockResult.value;
|
||||||
|
vi.mocked(IntersectionObserver).mock.calls[n][0](
|
||||||
|
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
||||||
|
// just provides the bare basics current implementation uses.
|
||||||
|
intersecting ? [{ isIntersecting: true } as IntersectionObserverEntry] : [],
|
||||||
|
observer,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const createSlotHost = (options?: {
|
export const createSlotHost = (options?: {
|
||||||
slot?: HTMLSlotElement;
|
slot?: HTMLSlotElement;
|
||||||
children?: HTMLElement[];
|
children?: HTMLElement[];
|
||||||
@@ -370,6 +402,12 @@ export const createParent = (options?: { children?: HTMLElement[] }): HTMLElemen
|
|||||||
return parent;
|
return parent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createLitElement = (): LitElement => {
|
||||||
|
const element = document.createElement('div') as unknown as LitElement;
|
||||||
|
element.requestUpdate = vi.fn();
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
export const createCardAPI = (): CardController => {
|
export const createCardAPI = (): CardController => {
|
||||||
const api = mock<CardController>();
|
const api = mock<CardController>();
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import {
|
|||||||
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../../../../src/utils/media-info';
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../../../../../src/utils/media-info';
|
||||||
import {
|
import {
|
||||||
IntersectionObserverMock,
|
IntersectionObserverMock,
|
||||||
|
callIntersectionHandler,
|
||||||
createMediaLoadedInfo,
|
createMediaLoadedInfo,
|
||||||
createParent,
|
createParent,
|
||||||
} from '../../../../test-utils';
|
} from '../../../../test-utils';
|
||||||
import {
|
import {
|
||||||
callEmblaHandler,
|
callEmblaHandler,
|
||||||
callIntersectionHandler,
|
|
||||||
callVisibilityHandler,
|
callVisibilityHandler,
|
||||||
createEmblaApiInstance,
|
createEmblaApiInstance,
|
||||||
createTestEmblaOptionHandler,
|
createTestEmblaOptionHandler,
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import AutoSize from '../../../../../src/utils/embla/plugins/auto-size/auto-size
|
|||||||
import {
|
import {
|
||||||
IntersectionObserverMock,
|
IntersectionObserverMock,
|
||||||
ResizeObserverMock,
|
ResizeObserverMock,
|
||||||
|
callIntersectionHandler,
|
||||||
createParent,
|
createParent,
|
||||||
requestAnimationFrameMock,
|
requestAnimationFrameMock,
|
||||||
} from '../../../../test-utils';
|
} from '../../../../test-utils';
|
||||||
import {
|
import {
|
||||||
callEmblaHandler,
|
callEmblaHandler,
|
||||||
callIntersectionHandler,
|
|
||||||
callResizeHandler,
|
callResizeHandler,
|
||||||
createEmblaApiInstance,
|
createEmblaApiInstance,
|
||||||
createTestEmblaOptionHandler,
|
createTestEmblaOptionHandler,
|
||||||
|
|||||||
@@ -43,20 +43,6 @@ export const callVisibilityHandler = (): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const callIntersectionHandler = (intersecting = true, n = 0): void => {
|
|
||||||
const mockResult = vi.mocked(IntersectionObserver).mock.results[n];
|
|
||||||
if (mockResult.type !== 'return') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const observer = mockResult.value;
|
|
||||||
vi.mocked(IntersectionObserver).mock.calls[n][0](
|
|
||||||
// Note this is a very incomplete / invalid IntersectionObserverEntry that
|
|
||||||
// just provides the bare basics current implementation uses.
|
|
||||||
intersecting ? [{ isIntersecting: true } as IntersectionObserverEntry] : [],
|
|
||||||
observer,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const callMutationHandler = (n = 0): void => {
|
export const callMutationHandler = (n = 0): void => {
|
||||||
const mockResult = vi.mocked(MutationObserver).mock.results[n];
|
const mockResult = vi.mocked(MutationObserver).mock.results[n];
|
||||||
if (mockResult.type !== 'return') {
|
if (mockResult.type !== 'return') {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
|||||||
'camera-manager/utils/*.ts',
|
'camera-manager/utils/*.ts',
|
||||||
'card-controller/*.ts',
|
'card-controller/*.ts',
|
||||||
'components-lib/cached-value-controller.ts',
|
'components-lib/cached-value-controller.ts',
|
||||||
|
'components-lib/live/live-controller.ts',
|
||||||
'components-lib/media-filter-controller.ts',
|
'components-lib/media-filter-controller.ts',
|
||||||
'components-lib/menu-button-controller.ts',
|
'components-lib/menu-button-controller.ts',
|
||||||
'components-lib/menu-controller.ts',
|
'components-lib/menu-controller.ts',
|
||||||
|
|||||||
Reference in New Issue
Block a user