Modify timeline to allow pan into past/future.
This commit is contained in:
@@ -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,37 +458,38 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const canSeek = !!this.view?.isViewerView();
|
||||
const newResults = this._locked
|
||||
? null
|
||||
: results
|
||||
.clone()
|
||||
.resetSelectedResult()
|
||||
.selectBestResult((media) =>
|
||||
findClosestMediaIndex(
|
||||
media,
|
||||
targetTime,
|
||||
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
||||
),
|
||||
);
|
||||
const canSeek = this._shouldSupportSeeking();
|
||||
const newResults =
|
||||
this._panBehavior === 'seek-in-media'
|
||||
? null
|
||||
: results
|
||||
.clone()
|
||||
.resetSelectedResult()
|
||||
.selectBestResult((media) =>
|
||||
findClosestMediaIndex(
|
||||
media,
|
||||
targetTime,
|
||||
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
canSeek ||
|
||||
(newResults &&
|
||||
newResults.hasSelectedResult() &&
|
||||
newResults.getResult() !== results.getResult())
|
||||
) {
|
||||
this.view
|
||||
.evolve({
|
||||
...(newResults &&
|
||||
newResults.hasSelectedResult() && { queryResults: newResults }),
|
||||
}) // Whether or not to set the timeline window.
|
||||
.mergeInContext({
|
||||
...(canSeek && { mediaViewer: { seek: targetTime } }),
|
||||
...this._setWindowInContext(properties),
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
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._getTimelineContext(properties),
|
||||
})
|
||||
.dispatchChangeEvent(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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 }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+18
-12
@@ -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,19 +214,25 @@ export const findClosestMediaIndex = (
|
||||
| undefined;
|
||||
|
||||
for (const [i, media] of mediaArray.entries()) {
|
||||
if (media.includesTime(targetTime)) {
|
||||
const start = media.getStartTime();
|
||||
const end = media.getEndTime();
|
||||
if (!refPoint || !start || !end) {
|
||||
if (!refPoint) {
|
||||
if (media.includesTime(targetTime)) {
|
||||
return i;
|
||||
}
|
||||
const delta =
|
||||
refPoint === 'end'
|
||||
? end.getTime() - targetTime.getTime()
|
||||
: targetTime.getTime() - start.getTime();
|
||||
if (!bestMatch || delta < bestMatch.delta) {
|
||||
bestMatch = { index: i, delta: delta };
|
||||
}
|
||||
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 (delta > 0 && (!bestMatch || delta < bestMatch.delta)) {
|
||||
bestMatch = { index: i, delta: delta };
|
||||
}
|
||||
}
|
||||
return bestMatch ? bestMatch.index : null;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user