Merge pull request #1399 from dermotduffy/timeline-pan-or-seek

Add ability to set timeline pan mode via config
This commit is contained in:
Dermot Duffy
2024-03-10 20:46:58 -07:00
committed by GitHub
11 changed files with 144 additions and 85 deletions
+6 -1
View File
@@ -632,6 +632,7 @@ live:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `pan_mode` | `pan`| :white_check_mark: | If set to `pan` then dragging the timeline will have no effect on the media. If set to `seek` will open media for that time and seek to that position . If set to `seek-in-media` will seek within the opened media only. If set to `seek-in-camera` will seek within the selected camera only. |
| `style` | `ribbon` | :white_check_mark: | Whether the timeline should show events as a single flat `ribbon` or a `stack` of events that are clustered using the `clustering_threshold` (below). |
| `window_seconds` | `3600` | :white_check_mark: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
| `clustering_threshold` | `3` | :white_check_mark: | The minimum number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. Depending on the timescale/zoom of the timeline, the underlying timeline library may still allow overlaps for low values of this parameter -- for a fully "flat" timeline use the `ribbon` style. `0` disables clustering entirely. Only used in the `stack` style of timeline. |
@@ -814,7 +815,9 @@ media_viewer:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the
`left` or `right` of the media or to hide it entirely (`none`).|
| `pan_mode` | `pan`| :white_check_mark: | If set to `pan` then dragging the timeline will have no effect on the media. If set to `seek` will open media for that time and seek to that position . If set to `seek-in-media` will seek within the opened media only. If set to `seek-in-camera` will seek within the selected camera only. |
| `style` | `ribbon` | :heavy_multiplication_x: | Whether the timeline should show events as a single flat `ribbon` or a `stack` of events that are clustered using the `clustering_threshold` (below). |
| `window_seconds` | `3600` | :heavy_multiplication_x: | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. |
| `clustering_threshold` | `3` | :heavy_multiplication_x: | The minimum number of overlapping events to allow prior to clustering/grouping them. Higher numbers cause clustering to happen less frequently. Depending on the timescale/zoom of the timeline, the underlying timeline library may still allow overlaps for low values of this parameter -- for a fully "flat" timeline use the `ribbon` style. `0` disables clustering entirely. Only used in the `stack` style of timeline. |
@@ -2139,6 +2142,7 @@ live:
timeline:
style: ribbon
mode: none
pan_mode: pan
clustering_threshold: 3
events_media_type: all
show_recordings: true
@@ -2232,6 +2236,7 @@ media_viewer:
timeline:
style: ribbon
mode: none
pan_mode: pan
clustering_threshold: 3
events_media_type: all
show_recordings: true
+31 -30
View File
@@ -39,6 +39,7 @@ import {
FrigateCardView,
ThumbnailsControlBaseConfig,
TimelineCoreConfig,
TimelinePanMode,
} from '../config/types';
import { localize } from '../localize/localize';
import timelineCoreStyle from '../scss/timeline-core.scss';
@@ -86,11 +87,9 @@ interface TimelineRangeChange extends TimelineWindow {
interface TimelineViewContext {
window?: TimelineWindow;
panBehavior?: TimelinePanBehavior;
}
type TimelineItemClickAction = 'play' | 'select';
type TimelinePanBehavior = 'pan' | 'seek' | 'seek-in-media' | 'seek-in-camera';
declare module 'view' {
interface ViewContext {
@@ -214,7 +213,7 @@ export class FrigateCardTimelineCore extends LitElement {
public itemClickAction?: TimelineItemClickAction;
@state()
protected _panBehavior: TimelinePanBehavior = 'seek';
protected _panMode: TimelinePanMode | null = null;
protected _targetBarVisible = false;
@@ -288,20 +287,22 @@ export class FrigateCardTimelineCore extends LitElement {
}
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
const panMode = this._getEffectivePanMode();
const panTitle =
this._panBehavior === 'pan'
? localize('timeline.pan_behavior.pan')
: this._panBehavior === 'seek'
? localize('timeline.pan_behavior.seek')
: this._panBehavior === 'seek-in-media'
? localize('timeline.pan_behavior.seek-in-media')
: localize('timeline.pan_behavior.seek-in-camera');
panMode === 'pan'
? localize('config.common.controls.timeline.pan_modes.pan')
: panMode === 'seek'
? localize('config.common.controls.timeline.pan_modes.seek')
: panMode === 'seek-in-media'
? localize('config.common.controls.timeline.pan_modes.seek-in-media')
: localize('config.common.controls.timeline.pan_modes.seek-in-camera');
const panIcon =
this._panBehavior === 'pan'
panMode === 'pan'
? 'mdi:pan-horizontal'
: this._panBehavior === 'seek'
: panMode === 'seek'
? 'mdi:filmstrip-box-multiple'
: this._panBehavior === 'seek-in-media'
: panMode === 'seek-in-media'
? 'mdi:play-box-lock'
: 'mdi:camera-lock';
@@ -318,12 +319,12 @@ export class FrigateCardTimelineCore extends LitElement {
? html` <ha-icon
.icon=${panIcon}
@click=${() => {
this._panBehavior =
this._panBehavior === 'pan'
this._panMode =
panMode === 'pan'
? 'seek'
: this._panBehavior === 'seek'
: panMode === 'seek'
? 'seek-in-media'
: this._panBehavior === 'seek-in-media'
: panMode === 'seek-in-media'
? 'seek-in-camera'
: 'pan';
}}
@@ -404,15 +405,15 @@ export class FrigateCardTimelineCore extends LitElement {
return;
}
const panMode = this._getEffectivePanMode();
const targetBarOn =
this._shouldSupportSeeking() &&
(this._panBehavior === 'seek' ||
this._panBehavior === 'seek-in-camera' ||
(this._panBehavior === 'seek-in-media' &&
(panMode === 'seek' ||
((panMode === 'seek-in-camera' || panMode === 'seek-in-media') &&
this._timeline.getSelection().some((id) => {
const item = this._timelineSource?.dataset?.get(id);
return (
this._panBehavior !== 'seek-in-camera' ||
panMode !== 'seek-in-camera' ||
item?.media?.getCameraID() === this.view?.camera,
item &&
item.start &&
@@ -473,6 +474,7 @@ export class FrigateCardTimelineCore extends LitElement {
): Promise<void> {
const results = this.view?.queryResults;
const media = results?.getResults();
const panMode = this._getEffectivePanMode();
if (
!media ||
!results ||
@@ -480,7 +482,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this.view ||
!this.hass ||
!this.cameraManager ||
this._panBehavior === 'pan'
panMode === 'pan'
) {
return;
}
@@ -488,7 +490,7 @@ export class FrigateCardTimelineCore extends LitElement {
const canSeek = this._shouldSupportSeeking();
let newResults: MediaQueriesResults | null = null;
if (this._panBehavior === 'seek') {
if (panMode === 'seek') {
newResults = results
.clone()
.resetSelectedResult()
@@ -499,7 +501,7 @@ export class FrigateCardTimelineCore extends LitElement {
main: true,
},
);
} else if (this._panBehavior === 'seek-in-camera') {
} else if (panMode === 'seek-in-camera') {
newResults = results
.clone()
.resetSelectedResult()
@@ -507,7 +509,7 @@ export class FrigateCardTimelineCore extends LitElement {
cameraID: this.view.camera,
})
.promoteCameraSelectionToMainSelection(this.view.camera);
} else if (this._panBehavior === 'seek-in-media') {
} else if (panMode === 'seek-in-media') {
newResults = results;
}
@@ -531,6 +533,10 @@ export class FrigateCardTimelineCore extends LitElement {
.dispatchChangeEvent(this);
}
protected _getEffectivePanMode(): TimelinePanMode {
return this._panMode ?? this.timelineConfig?.pan_mode ?? 'pan';
}
/**
* Called whenever the timeline is clicked.
* @param properties The properties of the timeline click event.
@@ -988,10 +994,6 @@ 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 (mediaWindow && !rangesOverlap(mediaWindow, timelineWindow)) {
@@ -1107,7 +1109,6 @@ export class FrigateCardTimelineCore extends LitElement {
return {
timeline: {
...this.view?.context?.timeline,
panBehavior: this._panBehavior,
...(newWindow && { window: newWindow }),
},
};
+12
View File
@@ -727,8 +727,17 @@ const timelineCoreConfigDefault = {
window_seconds: 60 * 60,
show_recordings: true,
style: 'stack' as const,
pan_mode: 'pan' as const,
};
export const timelinePanModeSchema = z.enum([
'pan',
'seek',
'seek-in-media',
'seek-in-camera',
]);
export type TimelinePanMode = z.infer<typeof timelinePanModeSchema>;
const timelineCoreConfigSchema = z.object({
clustering_threshold: z
.number()
@@ -748,6 +757,9 @@ const timelineCoreConfigSchema = z.object({
.optional()
.default(timelineCoreConfigDefault.show_recordings),
style: z.enum(['stack', 'ribbon']).optional().default(timelineCoreConfigDefault.style),
pan_mode: timelinePanModeSchema
.optional()
.default(timelineCoreConfigDefault.pan_mode),
});
export type TimelineCoreConfig = z.infer<typeof timelineCoreConfigSchema>;
+4
View File
@@ -163,6 +163,8 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE =
`${CONF_MEDIA_VIEWER}.controls.timeline.events_media_type` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE =
`${CONF_MEDIA_VIEWER}.controls.timeline.mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE =
`${CONF_MEDIA_VIEWER}.controls.timeline.pan_mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS =
`${CONF_MEDIA_VIEWER}.controls.timeline.show_recordings` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE =
@@ -219,6 +221,8 @@ export const CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE =
`${CONF_LIVE}.controls.timeline.events_media_type` as const;
export const CONF_LIVE_CONTROLS_TIMELINE_MODE =
`${CONF_LIVE}.controls.timeline.mode` as const;
export const CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE =
`${CONF_LIVE}.controls.timeline.pan_mode` as const;
export const CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS =
`${CONF_LIVE}.controls.timeline.show_recordings` as const;
export const CONF_LIVE_CONTROLS_TIMELINE_STYLE =
+52 -22
View File
@@ -91,6 +91,7 @@ import {
CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE,
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA_TYPE,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
@@ -140,6 +141,7 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
@@ -719,6 +721,26 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
},
];
protected _timelinePanModes: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: 'pan',
label: localize('config.common.controls.timeline.pan_modes.pan'),
},
{
value: 'seek',
label: localize('config.common.controls.timeline.pan_modes.seek'),
},
{
value: 'seek-in-media',
label: localize('config.common.controls.timeline.pan_modes.seek-in-media'),
},
{
value: 'seek-in-camera',
label: localize('config.common.controls.timeline.pan_modes.seek-in-camera'),
},
];
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -1197,30 +1219,34 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathTimelineEventsMediaType: string,
configPathShowRecordings: string,
defaultShowRecordings: boolean,
configPathPanMode?: string,
): TemplateResult {
return html` ${this._renderOptionSelector(
configPathStyle,
this._timelineStyleTypes,
{
return html`
${this._renderOptionSelector(configPathStyle, this._timelineStyleTypes, {
label: localize(`config.common.${CONF_TIMELINE_STYLE}`),
},
)}
${this._renderNumberInput(configPathWindowSeconds, {
label: localize(`config.common.${CONF_TIMELINE_WINDOW_SECONDS}`),
})}
${this._renderNumberInput(configPathClusteringThreshold, {
label: localize(`config.common.${CONF_TIMELINE_CLUSTERING_THRESHOLD}`),
})}
${this._renderOptionSelector(
configPathTimelineEventsMediaType,
this._timelineEventsMediaTypes,
{
label: localize(`config.common.${CONF_TIMELINE_EVENTS_MEDIA_TYPE}`),
},
)}
${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, {
label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`),
})}`;
})}
${configPathPanMode
? this._renderOptionSelector(configPathPanMode, this._timelinePanModes, {
label: localize(`config.common.controls.timeline.pan_mode`),
})
: ``}
${this._renderNumberInput(configPathWindowSeconds, {
label: localize(`config.common.${CONF_TIMELINE_WINDOW_SECONDS}`),
})}
${this._renderNumberInput(configPathClusteringThreshold, {
label: localize(`config.common.${CONF_TIMELINE_CLUSTERING_THRESHOLD}`),
})}
${this._renderOptionSelector(
configPathTimelineEventsMediaType,
this._timelineEventsMediaTypes,
{
label: localize(`config.common.${CONF_TIMELINE_EVENTS_MEDIA_TYPE}`),
},
)}
${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, {
label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`),
})}
`;
}
/**
@@ -1241,6 +1267,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathTimelineEventsMediaType: string,
configPathShowRecordings: string,
showRecordingsDefault: boolean,
configPathPanMode: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
@@ -1257,6 +1284,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathTimelineEventsMediaType,
configPathShowRecordings,
showRecordingsDefault,
configPathPanMode,
)}`,
);
}
@@ -2208,6 +2236,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
this._defaults.live.controls.timeline.show_recordings,
CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE,
)}
${this._putInSubmenu(
MENU_LIVE_CONTROLS_PTZ,
@@ -2394,6 +2423,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
this._defaults.media_viewer.controls.timeline.show_recordings,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE,
)}
`,
)}
+8 -7
View File
@@ -173,6 +173,13 @@
"above": "Above",
"below": "Below",
"none": "None"
},
"pan_mode": "Default pan mode",
"pan_modes": {
"pan": "Pan",
"seek": "Pan seeks across all media",
"seek-in-camera": "Pan seeks within selected camera only",
"seek-in-media": "Pan seeks within selected media item only"
}
},
"title": {
@@ -589,12 +596,6 @@
"timeline": "See media in timeline"
},
"timeline": {
"pan_behavior": {
"pan": "Pan",
"seek": "Pan seeks across all media",
"seek-in-camera": "Pan seeks within selected camera only",
"seek-in-media": "Pan seeks within selected media item only"
},
"select_date": "Choose date"
}
}
}
+8 -7
View File
@@ -28,8 +28,8 @@
"editor_label": "Opzioni di dipendenza"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"editor_label": "",
"layout": {
"fit": "Adatta al layout",
"fits": {
@@ -172,6 +172,13 @@
"above": "sopra",
"below": "sotto",
"none": "sessuna"
},
"pan_mode": "",
"pan_modes": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
@@ -577,12 +584,6 @@
"timeline": "Vedi evento nella timeline"
},
"timeline": {
"pan_behavior": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
},
"select_date": "Scegli la data"
}
}
+8 -7
View File
@@ -28,8 +28,8 @@
"editor_label": "Opções de dependência"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"editor_label": "",
"layout": {
"fit": "Ajuste de layout",
"fits": {
@@ -172,6 +172,13 @@
"above": "Acima",
"below": "Abaixo",
"none": "Nenhum"
},
"pan_mode": "",
"pan_modes": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
@@ -587,12 +594,6 @@
"timeline": "Ver evento na linha do tempo"
},
"timeline": {
"pan_behavior": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
},
"select_date": "Escolha a data"
}
}
+9 -8
View File
@@ -28,8 +28,8 @@
"editor_label": "Opções de dependência"
},
"dimensions": {
"editor_label": "",
"aspect_ratio": "",
"editor_label": "",
"layout": {
"fit": "Fit",
"fits": {
@@ -102,9 +102,9 @@
"editor_label": "Opções de activação",
"entities": "Activar a partir de outras entidades",
"events": {
"clips": "",
"editor_label": "",
"events": "",
"clips": "",
"snapshots": ""
},
"motion": "Activar detectando automaticamente o sensor de movimento",
@@ -172,6 +172,13 @@
"above": "Por cima",
"below": "Abaixo",
"none": "Nenhum"
},
"pan_mode": "",
"pan_modes": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
@@ -569,12 +576,6 @@
"timeline": "Ver evento na linha do tempo"
},
"timeline": {
"pan_behavior": {
"pan": "",
"seek": "",
"seek-in-camera": "",
"seek-in-media": ""
},
"select_date": "Selecionar a data"
}
}
+3
View File
@@ -91,6 +91,7 @@ describe('config defaults', () => {
clustering_threshold: 3,
events_media_type: 'all',
mode: 'none',
pan_mode: 'pan',
show_recordings: true,
style: 'ribbon',
window_seconds: 3600,
@@ -147,6 +148,7 @@ describe('config defaults', () => {
clustering_threshold: 3,
events_media_type: 'all',
mode: 'none',
pan_mode: 'pan',
show_recordings: true,
style: 'ribbon',
window_seconds: 3600,
@@ -271,6 +273,7 @@ describe('config defaults', () => {
},
},
events_media_type: 'all',
pan_mode: 'pan',
show_recordings: true,
style: 'stack',
window_seconds: 3600,
+3 -3
View File
@@ -80,13 +80,13 @@ export default defineConfig({
// Expected thresholds for anything that does not have 100% coverage
// yet.
statements: 10.67,
statements: 10.69,
branches: 4.9,
functions: 7.81,
lines: 10.92,
lines: 10.94,
...calculateFullCoverageThresholds(),
},
},
},
});
});