Refactor views to support dynamic updates.

This commit is contained in:
Dermot Duffy
2024-07-26 19:15:38 -07:00
parent 33dee47205
commit d5c4e56c45
80 changed files with 3714 additions and 4021 deletions
+2 -92
View File
@@ -1,26 +1,15 @@
import { sub } from 'date-fns';
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>;
fetchThumbnails?: boolean;
}
declare module 'view' {
@@ -36,8 +25,7 @@ interface LastMediaLoadedInfo {
type LiveControllerHost = LitElement &
FrigateCardMediaLoadedEventTarget &
FrigateCardMessageEventTarget &
FrigateCardViewChangeEventTarget;
FrigateCardMessageEventTarget;
export class LiveController implements ReactiveController {
protected _host: LiveControllerHost;
@@ -71,7 +59,7 @@ export class LiveController implements ReactiveController {
// 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;
return !(this._inBackground && this._messageReceived);
}
public hostConnected(): void {
@@ -79,7 +67,6 @@ export class LiveController implements ReactiveController {
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 {
@@ -87,7 +74,6 @@ export class LiveController implements ReactiveController {
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 {
@@ -124,12 +110,6 @@ export class LiveController implements ReactiveController {
}
};
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);
@@ -150,74 +130,4 @@ export class LiveController implements ReactiveController {
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,
},
);
}
}
}
+31 -41
View File
@@ -21,10 +21,9 @@ import { SelectOption, SelectValues } from '../components/select';
import { CardWideConfig } from '../config/types';
import { localize } from '../localize/localize';
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
import { executeMediaQueryForViewWithErrorDispatching } from '../utils/media-to-view';
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { View } from '../view/view';
import { ViewManagerInterface } from '../card-controller/view/types';
interface MediaFilterControls {
events: boolean;
@@ -77,6 +76,7 @@ export class MediaFilterController {
protected _favoriteOptions: SelectOption[];
protected _defaults: MediaFilterCoreDefaults | null = null;
protected _viewManager: ViewManagerInterface | null = null;
constructor(host: LitElement) {
this._host = host;
@@ -154,10 +154,12 @@ export class MediaFilterController {
public getDefaults(): MediaFilterCoreDefaults | null {
return this._defaults;
}
public setViewManager(viewManager: ViewManagerInterface | null): void {
this._viewManager = viewManager;
}
public async valueChangeHandler(
cameraManager: CameraManager,
view: View,
cardWideConfig: CardWideConfig,
values: {
camera?: string | string[];
@@ -234,20 +236,15 @@ export class MediaFilterController {
},
]);
(
await executeMediaQueryForViewWithErrorDispatching(
this._host,
cameraManager,
view,
queries,
{
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView:
values.mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
},
)
)?.dispatchChangeEvent(this._host);
this._viewManager?.setViewByParametersWithExistingQuery({
params: {
query: queries,
// See 'A note on views' above for these two arguments
...(cameraIDs.size === 1 && { camera: [...cameraIDs][0] }),
view: values.mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
},
});
} else {
const queries = new RecordingMediaQueries([
{
@@ -262,19 +259,15 @@ export class MediaFilterController {
},
]);
(
await executeMediaQueryForViewWithErrorDispatching(
this._host,
cameraManager,
view,
queries,
{
// See 'A note on views' above for these two arguments.
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
targetView: 'recordings',
},
)
)?.dispatchChangeEvent(this._host);
this._viewManager?.setViewByParametersWithExistingQuery({
params: {
query: queries,
// See 'A note on views' above for these two arguments
...(cameraIDs.size === 1 && { camera: [...cameraIDs][0] }),
view: 'recordings',
},
});
}
// Need to ensure we update the element as the date-picker selections may
@@ -288,10 +281,11 @@ export class MediaFilterController {
});
}
public computeInitialDefaultsFromView(cameraManager: CameraManager, view: View): void {
const queries = view.query?.getQueries();
public computeInitialDefaultsFromView(cameraManager: CameraManager): void {
const view = this._viewManager?.getView();
const queries = view?.query?.getQueries();
const allCameraIDs = this._getAllCameraIDs(cameraManager);
if (!queries || !allCameraIDs.size) {
if (!view || !queries || !allCameraIDs.size) {
return;
}
@@ -444,14 +438,10 @@ export class MediaFilterController {
this._host.requestUpdate();
}
public getControlsToShow(
cameraManager: CameraManager,
view: View,
): MediaFilterControls {
const events = !!(view.query && MediaQueriesClassifier.areEventQueries(view.query));
const recordings = !!(
view.query && MediaQueriesClassifier.areRecordingQueries(view.query)
);
public getControlsToShow(cameraManager: CameraManager): MediaFilterControls {
const view = this._viewManager?.getView();
const events = MediaQueriesClassifier.areEventQueries(view?.query);
const recordings = MediaQueriesClassifier.areRecordingQueries(view?.query);
const managerCapabilities = cameraManager.getAggregateCameraCapabilities();
return {
+1 -1
View File
@@ -3,7 +3,7 @@ import { StyleInfo } from 'lit/directives/style-map';
import { CameraManager } from '../camera-manager/manager';
import { MediaPlayerManager } from '../card-controller/media-player-manager';
import { MicrophoneManager } from '../card-controller/microphone-manager';
import { ViewManager } from '../card-controller/view-manager';
import { ViewManager } from '../card-controller/view/view-manager';
import {
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig,
+16 -12
View File
@@ -1,6 +1,7 @@
import { ViewContext } from 'view';
import { dispatchViewContextChangeEvent } from '../../view/view.js';
import { ZoomSettingsObserved, PartialZoomSettings } from './types.js';
import { MergeContextViewModifier } from '../../card-controller/view/modifiers/merge-context.js';
import { ViewManagerInterface } from '../../card-controller/view/types.js';
import { PartialZoomSettings, ZoomSettingsObserved } from './types.js';
interface ZoomViewContext {
observed?: ZoomSettingsObserved;
@@ -38,19 +39,22 @@ export const generateViewContextForZoom = (
};
/**
* Convenience wrapper to convert zoom settings into a dispatched view context
* change.
* Convenience wrapper to convert zoom settings into a view change
*/
export const handleZoomSettingsObservedEvent = (
element: EventTarget,
ev: CustomEvent<ZoomSettingsObserved>,
viewManager?: ViewManagerInterface,
targetID?: string,
): void => {
targetID &&
dispatchViewContextChangeEvent(
element,
generateViewContextForZoom(targetID, {
observed: ev.detail,
}),
);
viewManager &&
targetID &&
viewManager.setViewByParameters({
modifiers: [
new MergeContextViewModifier(
generateViewContextForZoom(targetID, {
observed: ev.detail,
}),
),
],
});
};