Modify timeline to allow pan into past/future.

This commit is contained in:
Dermot Duffy
2023-04-08 12:51:10 -07:00
parent ecf954249d
commit b3bb56298a
6 changed files with 106 additions and 73 deletions
+49 -31
View File
@@ -76,9 +76,11 @@ interface TimelineRangeChange extends TimelineWindow {
interface TimelineViewContext {
window?: TimelineWindow;
panBehavior?: TimelinePanBehavior;
}
type TimelineItemClickAction = 'play' | 'select';
type TimelinePanBehavior = 'pan' | 'seek' | 'seek-in-media';
declare module 'view' {
interface ViewContext {
@@ -197,7 +199,7 @@ export class FrigateCardTimelineCore extends LitElement {
public itemClickAction?: TimelineItemClickAction;
@state()
protected _locked = false;
protected _panBehavior: TimelinePanBehavior = 'seek';
protected _targetBarVisible = false;
@@ -272,9 +274,18 @@ export class FrigateCardTimelineCore extends LitElement {
}
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
const lockTitle = this._locked
? localize('timeline.unlock')
: localize('timeline.lock');
const panTitle =
this._panBehavior === 'pan'
? localize('timeline.pan_behavior.pan')
: this._panBehavior === 'seek'
? localize('timeline.pan_behavior.seek')
: localize('timeline.pan_behavior.seek-in-media');
const panIcon =
this._panBehavior === 'pan'
? 'mdi:pan-horizontal'
: this._panBehavior === 'seek'
? 'mdi:movie-search'
: 'mdi:file-find';
return html` ${capabilities?.supportsTimeline
? html` <div
@@ -287,12 +298,17 @@ export class FrigateCardTimelineCore extends LitElement {
<div class="timeline-tools">
${this._shouldSupportSeeking()
? html` <ha-icon
.icon=${`mdi:${this._locked ? 'lock' : 'lock-open-variant'}`}
.icon=${panIcon}
@click=${() => {
this._locked = !this._locked;
this._panBehavior =
this._panBehavior === 'pan'
? 'seek'
: this._panBehavior === 'seek'
? 'seek-in-media'
: 'pan';
}}
aria-label="${lockTitle}"
title="${lockTitle}"
aria-label="${panTitle}"
title="${panTitle}"
>
</ha-icon>`
: ''}
@@ -340,9 +356,10 @@ export class FrigateCardTimelineCore extends LitElement {
}
if (
this._shouldSupportSeeking() &&
this._timeline &&
properties.byUser &&
// Do not adjust select children or seek during zoom events.
// Do not adjust select/seek media during zoom events.
properties.event.type !== 'wheel' &&
properties.event.additionalEvent !== 'pinchin' &&
properties.event.additionalEvent !== 'pinchout'
@@ -365,13 +382,7 @@ export class FrigateCardTimelineCore extends LitElement {
}
protected _shouldSupportSeeking(): boolean {
const cameraIDs = this._getTimelineCameraIDs();
if (!this._timeline || !cameraIDs) {
return false;
}
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
return (this.view?.isViewerView() && capabilities?.canSeek) ?? false;
return this.mini;
}
/**
@@ -385,8 +396,8 @@ export class FrigateCardTimelineCore extends LitElement {
const targetBarOn =
this._shouldSupportSeeking() &&
(!this._locked ||
(this.mini &&
(this._panBehavior === 'seek' ||
(this._panBehavior === 'seek-in-media' &&
this._timeline.getSelection().some((id) => {
const item = this._timelineSource?.dataset?.get(id);
return (
@@ -439,6 +450,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this.view ||
!this.hass ||
!this.cameraManager ||
this._panBehavior === 'pan' ||
// Skip range changes that do not have hammerjs pan directions associated
// with them, as these outliers cause media matching issues below.
!properties.event.additionalEvent
@@ -446,8 +458,9 @@ export class FrigateCardTimelineCore extends LitElement {
return;
}
const canSeek = !!this.view?.isViewerView();
const newResults = this._locked
const canSeek = this._shouldSupportSeeking();
const newResults =
this._panBehavior === 'seek-in-media'
? null
: results
.clone()
@@ -460,24 +473,24 @@ export class FrigateCardTimelineCore extends LitElement {
),
);
if (
canSeek ||
(newResults &&
newResults.hasSelectedResult() &&
newResults.getResult() !== results.getResult())
) {
const desiredView: FrigateCardView = this.mini
? targetTime >= new Date()
? 'live'
: 'media'
: this.view.view;
this.view
.evolve({
view: desiredView,
...(newResults &&
newResults.hasSelectedResult() && { queryResults: newResults }),
}) // Whether or not to set the timeline window.
.mergeInContext({
...(canSeek && { mediaViewer: { seek: targetTime } }),
...this._setWindowInContext(properties),
...this._getTimelineContext(properties),
})
.dispatchChangeEvent(this);
}
}
/**
* Called whenever the timeline is clicked.
@@ -667,7 +680,7 @@ export class FrigateCardTimelineCore extends LitElement {
// 'flicker' for the user as the viewer reloads all the media).
const newResults = newView?.queryResults;
if (newView && newResults && !this.view.queryResults?.isSupersetOf(newResults)) {
newView?.mergeInContext(this._setWindowInContext())?.dispatchChangeEvent(this);
newView?.mergeInContext(this._getTimelineContext())?.dispatchChangeEvent(this);
}
}
}
@@ -948,6 +961,10 @@ export class FrigateCardTimelineCore extends LitElement {
: null;
const context = this.view.context?.timeline;
if (context && context.panBehavior) {
this._panBehavior = context.panBehavior;
}
if (context && context.window) {
desiredWindow = context.window;
} else if (media && mediaWindow && !rangesOverlap(mediaWindow, timelineWindow)) {
@@ -1021,7 +1038,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this._alreadyHasAcceptableMediaQuery(freshMediaQuery)
) {
(await this._createViewWithEventMediaQuery(freshMediaQuery))
?.mergeInContext(this._setWindowInContext(desiredWindow))
?.mergeInContext(this._getTimelineContext(desiredWindow))
.dispatchChangeEvent(this);
}
}
@@ -1046,11 +1063,12 @@ export class FrigateCardTimelineCore extends LitElement {
* Generate the context for timeline views.
* @returns The TimelineViewContext object.
*/
protected _setWindowInContext(window?: TimelineWindow): ViewContext {
protected _getTimelineContext(window?: TimelineWindow): ViewContext {
const newWindow = window ?? this._timeline?.getWindow();
return {
timeline: {
...this.view?.context?.timeline,
panBehavior: this._panBehavior,
...(newWindow && { window: newWindow }),
},
};
+6 -3
View File
@@ -460,9 +460,12 @@
"timeline": "See media in timeline"
},
"timeline": {
"lock": "Lock timeline to a single event",
"select_date": "Choose date",
"unlock": "Unlock timeline"
"pan_behavior": {
"pan": "Pan",
"seek": "Pan seeks across media",
"seek-in-media": "Pan seeks within media"
},
"select_date": "Choose date"
},
"elements": {
"ptz": {
+6 -3
View File
@@ -450,9 +450,12 @@
"timeline": "Vedi evento nella timeline"
},
"timeline": {
"lock": "Blocca la sequenza temporale su un singolo evento",
"select_date": "Scegli la data",
"unlock": "Sblocca la cronologia"
"pan_behavior": {
"pan": "",
"seek": "",
"seek-in-media": ""
},
"select_date": "Scegli la data"
},
"elements": {
"ptz": {
+6 -3
View File
@@ -471,8 +471,11 @@
"timeline": "Ver evento na linha do tempo"
},
"timeline": {
"lock": "Bloquear linha do tempo em um único evento",
"select_date": "Escolha a data",
"unlock": "Desbloquear linha do tempo"
"pan_behavior": {
"pan": "",
"seek": "",
"seek-in-media": ""
},
"select_date": "Escolha a data"
}
}
+12 -6
View File
@@ -194,7 +194,7 @@ export const executeMediaQueryForView = async (
/**
* Find the closest matching media object.
* @param mediaArray The media. Must be sorted most recent first.
* @param mediaArray The media.
* @param targetTime The target time used to find the relevant child.
* @param refPoint Whether to find based on the start or end of the
* event/recording. If not specified, the first match is returned rather than
@@ -214,20 +214,26 @@ export const findClosestMediaIndex = (
| undefined;
for (const [i, media] of mediaArray.entries()) {
if (!refPoint) {
if (media.includesTime(targetTime)) {
const start = media.getStartTime();
const end = media.getEndTime();
if (!refPoint || !start || !end) {
return i;
}
continue;
}
const start = media.getStartTime();
const end = media.getEndTime() ?? start;
if (!start || !end) {
continue;
}
const delta =
refPoint === 'end'
? end.getTime() - targetTime.getTime()
: targetTime.getTime() - start.getTime();
if (!bestMatch || delta < bestMatch.delta) {
if (delta > 0 && (!bestMatch || delta < bestMatch.delta)) {
bestMatch = { index: i, delta: delta };
}
}
}
return bestMatch ? bestMatch.index : null;
};
+1 -1
View File
@@ -51,7 +51,7 @@ export class ViewMedia {
}
public includesTime(seek: Date): boolean {
const startTime = this.getStartTime();
const endTime = this.getEndTime();
const endTime = this.getEndTime() ?? startTime;
return !!startTime && !!endTime && seek >= startTime && seek <= endTime;
}