From f123bb91dd1ccd075b1269b06b9e2d03ab7c510b Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 1 Feb 2025 13:37:12 -0800 Subject: [PATCH] feat: Add support for 12-hour clock formatting on timelines (#1862) - Closes #1779 [skip ci] --- docs/configuration/live.md | 19 +++++++++++++++++ docs/configuration/media-viewer.md | 19 +++++++++++++++++ docs/configuration/timeline.md | 17 +++++++++++++++ src/components/timeline-core.ts | 24 ++++++++++++++++++++- src/config/types.ts | 8 +++++++ src/const.ts | 5 +++++ src/editor.ts | 34 ++++++++++++++++++++++++++++-- src/localize/languages/ca.json | 4 ++++ src/localize/languages/en.json | 4 ++++ src/localize/languages/fr.json | 4 ++++ src/localize/languages/it.json | 4 ++++ src/localize/languages/pt-BR.json | 4 ++++ src/localize/languages/pt-PT.json | 4 ++++ tests/config/types.test.ts | 9 ++++++++ 14 files changed, 156 insertions(+), 3 deletions(-) diff --git a/docs/configuration/live.md b/docs/configuration/live.md index f26e28f1..b726186a 100644 --- a/docs/configuration/live.md +++ b/docs/configuration/live.md @@ -118,6 +118,7 @@ live: | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clustering_threshold` | `3` | 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. | | `events_media_type` | `all` | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot. | +| `format` | | Configuration for the timeline time & date format. See below. | | `mode` | `none` | 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` | See [timeline pan mode](timeline-pan-mode.md). | | `show_recordings` | `true` | Whether to show recordings on the timeline (specifically: which hours have any recorded content). | @@ -126,6 +127,22 @@ live: [](common/timeline-seek-info.md ':include') +#### `format` + +Configure the date and time format for the `timeline` view. + +```yaml +live: + controls: + timeline: + format: + # [...] +``` + +| Option | Default | Description | +| ------ | ------- | ------------------------------------------------------------------------------- | +| `24h` | `true` | If `true` shows time in 24-hour clock. If `false` otherwise uses 12-hour clock. | + ## `display` Controls whether to show a single or grid `live` view. @@ -213,6 +230,8 @@ live: events_media_type: all show_recordings: true window_seconds: 3600 + format: + 24h: true microphone: always_connected: false disconnect_seconds: 90 diff --git a/docs/configuration/media-viewer.md b/docs/configuration/media-viewer.md index 0a4dcb0e..a43ef42e 100644 --- a/docs/configuration/media-viewer.md +++ b/docs/configuration/media-viewer.md @@ -112,6 +112,7 @@ media_viewer: | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clustering_threshold` | `3` | 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. | | `events_media_type` | `all` | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot. | +| `format` | | Configuration for the timeline time & date format. See below. | | `mode` | `none` | 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` | See [timeline pan mode](timeline-pan-mode.md). | | `show_recordings` | `true` | Whether to show recordings on the timeline (specifically: which hours have any recorded content). | @@ -120,6 +121,22 @@ media_viewer: [](common/timeline-seek-info.md ':include') +#### `format` + +Configure the date and time format for the `timeline` view. + +```yaml +media_viewer: + controls: + timeline: + format: + # [...] +``` + +| Option | Default | Description | +| ------ | ------- | ------------------------------------------------------------------------------- | +| `24h` | `true` | If `true` shows time in 24-hour clock. If `false` otherwise uses 12-hour clock. | + ## `display` Controls whether to show a single media item or grid in the media viewer. @@ -188,6 +205,8 @@ media_viewer: events_media_type: all show_recordings: true window_seconds: 3600 + format: + 24h: true display: mode: single grid_selected_width_factor: 2 diff --git a/docs/configuration/timeline.md b/docs/configuration/timeline.md index 047d4bc1..b854b1ed 100644 --- a/docs/configuration/timeline.md +++ b/docs/configuration/timeline.md @@ -19,6 +19,7 @@ You can interact with the timeline in a number of ways: | `clustering_threshold` | `3` | 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. | | `controls` | | Configuration for the timeline controls. See below. | | `events_media_type` | `all` | Whether to show only events with `clips`, events with `snapshots` or `all` events. When `all` is used, `clips` are favored for events that have both a clip and a snapshot. | +| `format` | | Configuration for the timeline time & date format. See below. | | `show_recordings` | `true` | Whether to show recordings on the timeline (specifically: which hours have any recorded content). | | `style` | `stack` | Whether the timeline should show events as a single flat `ribbon` or a `stack` of events that are clustered using the `clustering_threshold`. | | `window_seconds` | `3600` | The length of the default timeline in seconds. By default, 1 hour (`3600` seconds) is shown in the timeline. | @@ -37,6 +38,20 @@ timeline: | ------------ | ------- | ---------------------------------------------------------------------- | | `thumbnails` | | Configures how thumbnails are shown on the `timeline` view. See below. | +## `format` + +Configure the date and time format for the `timeline` view. + +```yaml +timeline: + format: + # [...] +``` + +| Option | Default | Description | +| ------ | ------- | ------------------------------------------------------------------------------- | +| `24h` | `true` | If `true` shows time in 24-hour clock. If `false` otherwise uses 12-hour clock. | + ### `thumbnails` Configures how thumbnails are shown on the timeline. @@ -68,6 +83,8 @@ timeline: events_media_type: all show_recordings: true window_seconds: 3600 + format: + 24h: true controls: thumbnails: mode: left diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index d0525222..058ae3ba 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -13,7 +13,12 @@ import isEqual from 'lodash-es/isEqual'; import throttle from 'lodash-es/throttle'; import { ViewContext } from 'view'; import { DataSet } from 'vis-data/esnext'; -import type { DataGroupCollectionType, DateType, IdType } from 'vis-timeline/esnext'; +import type { + DataGroupCollectionType, + DateType, + IdType, + TimelineFormatOption, +} from 'vis-timeline/esnext'; import { Timeline, TimelineEventPropertiesResult, @@ -814,6 +819,22 @@ export class FrigateCardTimelineCore extends LitElement { ); } + protected _getDateTimeFormat(): TimelineFormatOption { + const format24Hour = !!this.timelineConfig?.format?.['24h']; + + // See: https://visjs.github.io/vis-timeline/docs/timeline/#Configuration_Options + return { + minorLabels: { + minute: format24Hour ? 'HH:mm' : 'h:mm A', + hour: format24Hour ? 'HH:mm' : 'h:mm A', + }, + majorLabels: { + millisecond: format24Hour ? 'HH:mm:ss' : 'h:mm:ss A', + second: format24Hour ? 'D MMMM HH:mm' : 'D MMMM h:mm A', + }, + }; + } + /** * Get timeline options. */ @@ -883,6 +904,7 @@ export class FrigateCardTimelineCore extends LitElement { overflowMethod: 'cap', template: this._getTooltip.bind(this), }, + format: this._getDateTimeFormat(), xss: { disabled: false, filterOptions: { diff --git a/src/config/types.ts b/src/config/types.ts index 71878a76..4cf393e3 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1060,11 +1060,18 @@ const timelineCoreConfigDefault = { show_recordings: true, style: 'stack' as const, pan_mode: 'pan' as const, + format: { + '24h': true, + }, }; const timelinePanModeSchema = z.enum(['pan', 'seek', 'seek-in-media', 'seek-in-camera']); export type TimelinePanMode = z.infer; +const timelineFormatSchema = z.object({ + '24h': z.boolean().optional().default(timelineCoreConfigDefault.format['24h']), +}); + const timelineCoreConfigSchema = z.object({ clustering_threshold: z .number() @@ -1085,6 +1092,7 @@ const timelineCoreConfigSchema = z.object({ .default(timelineCoreConfigDefault.show_recordings), style: z.enum(['stack', 'ribbon']).optional().default(timelineCoreConfigDefault.style), pan_mode: timelinePanModeSchema.optional().default(timelineCoreConfigDefault.pan_mode), + format: timelineFormatSchema.optional().default(timelineCoreConfigDefault.format), }); export type TimelineCoreConfig = z.infer; diff --git a/src/const.ts b/src/const.ts index 321086b5..6b121100 100644 --- a/src/const.ts +++ b/src/const.ts @@ -224,6 +224,8 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD = `${CONF_MEDIA_VIEWER}.controls.timeline.clustering_threshold` as const; 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_FORMAT_24H = + `${CONF_MEDIA_VIEWER}.controls.timeline.format.24h` 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 = @@ -277,6 +279,8 @@ export const CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD = `${CONF_LIVE}.controls.timeline.clustering_threshold` as const; export const CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE = `${CONF_LIVE}.controls.timeline.events_media_type` as const; +export const CONF_LIVE_CONTROLS_TIMELINE_FORMAT_24H = + `${CONF_LIVE}.controls.timeline.format.24h` as const; export const CONF_LIVE_CONTROLS_TIMELINE_MODE = `${CONF_LIVE}.controls.timeline.mode` as const; export const CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE = @@ -322,6 +326,7 @@ export const CONF_TIMELINE_CLUSTERING_THRESHOLD = `${CONF_TIMELINE}.clustering_threshold` as const; export const CONF_TIMELINE_EVENTS_MEDIA_TYPE = `${CONF_TIMELINE}.events_media_type` as const; +export const CONF_TIMELINE_FORMAT_24H = `${CONF_TIMELINE}.format.24h` as const; export const CONF_TIMELINE_SHOW_RECORDINGS = `${CONF_TIMELINE}.show_recordings` as const; export const CONF_TIMELINE_STYLE = `${CONF_TIMELINE}.style` as const; export const CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE = diff --git a/src/editor.ts b/src/editor.ts index 7c73d1c1..a3511ee4 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -120,6 +120,7 @@ import { CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD, CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE, + CONF_LIVE_CONTROLS_TIMELINE_FORMAT_24H, CONF_LIVE_CONTROLS_TIMELINE_MODE, CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE, CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS, @@ -160,6 +161,7 @@ import { CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT_24H, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS, @@ -200,6 +202,7 @@ import { CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, CONF_TIMELINE_EVENTS_MEDIA_TYPE, + CONF_TIMELINE_FORMAT_24H, CONF_TIMELINE_SHOW_RECORDINGS, CONF_TIMELINE_STYLE, CONF_TIMELINE_WINDOW_SECONDS, @@ -278,6 +281,7 @@ const MENU_OPTIONS = 'options'; const MENU_PERFORMANCE_FEATURES = 'performance.features'; const MENU_PERFORMANCE_STYLE = 'performance.style'; const MENU_STATUS_BAR_ITEMS = 'status_bar.items'; +const MENU_TIMELINE_FORMAT = 'timeline.format'; const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails'; const MENU_VIEW_DEFAULT_RESET = 'view.default_reset'; const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts'; @@ -1535,12 +1539,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor * @returns A rendered template. */ protected _renderTimelineCoreControls( + domain: string, configPathStyle: string, configPathWindowSeconds: string, configPathClusteringThreshold: string, configPathTimelineEventsMediaType: string, configPathShowRecordings: string, + configPathFormat24h: string, defaultShowRecordings: boolean, + defaultFormat24h: boolean, configPathPanMode?: string, ): TemplateResult { return html` @@ -1568,6 +1575,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderSwitch(configPathShowRecordings, defaultShowRecordings, { label: localize(`config.common.${CONF_TIMELINE_SHOW_RECORDINGS}`), })} + ${this._putInSubmenu( + `${domain}.format`, + true, + 'config.common.controls.timeline.format.editor_label', + 'mdi:clock-edit', + html` + ${this._renderSwitch(configPathFormat24h, defaultFormat24h, { + label: localize('config.common.controls.timeline.format.24h'), + })} + `, + )} `; } @@ -1588,7 +1606,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor configPathClusteringThreshold: string, configPathTimelineEventsMediaType: string, configPathShowRecordings: string, - showRecordingsDefault: boolean, + configPathFormat24h: string, + defaultShowRecordings: boolean, + defaultFormat24h: boolean, configPathPanMode: string, ): TemplateResult | void { return this._putInSubmenu( @@ -1600,12 +1620,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor label: localize('config.common.controls.timeline.mode'), })} ${this._renderTimelineCoreControls( + domain, configPathStyle, configPathWindowSeconds, configPathClusteringThreshold, configPathTimelineEventsMediaType, configPathShowRecordings, - showRecordingsDefault, + configPathFormat24h, + defaultShowRecordings, + defaultFormat24h, configPathPanMode, )}`, ); @@ -2722,7 +2745,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD, CONF_LIVE_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE, CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS, + CONF_LIVE_CONTROLS_TIMELINE_FORMAT_24H, this._defaults.live.controls.timeline.show_recordings, + this._defaults.live.controls.timeline.format['24h'], CONF_LIVE_CONTROLS_TIMELINE_PAN_MODE, )} ${this._putInSubmenu( @@ -2904,7 +2929,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_EVENTS_MEDIA_TYPE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_FORMAT_24H, this._defaults.media_viewer.controls.timeline.show_recordings, + this._defaults.media_viewer.controls.timeline.format['24h'], CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_PAN_MODE, )} `, @@ -2927,12 +2954,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._expandedMenus[MENU_OPTIONS] === 'timeline' ? html`
${this._renderTimelineCoreControls( + MENU_TIMELINE_FORMAT, CONF_TIMELINE_STYLE, CONF_TIMELINE_WINDOW_SECONDS, CONF_TIMELINE_CLUSTERING_THRESHOLD, CONF_TIMELINE_EVENTS_MEDIA_TYPE, CONF_TIMELINE_SHOW_RECORDINGS, + CONF_TIMELINE_FORMAT_24H, this._defaults.timeline.show_recordings, + this._defaults.timeline.format['24h'], )} ${this._renderThumbnailsControls( MENU_TIMELINE_CONTROLS_THUMBNAILS, diff --git a/src/localize/languages/ca.json b/src/localize/languages/ca.json index eb297cb6..0bd3df7a 100644 --- a/src/localize/languages/ca.json +++ b/src/localize/languages/ca.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Mini cronologia", + "format": { + "24h": "", + "editor_label": "" + }, "mode": "Mode", "modes": { "above": "A dalt", diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index e96fe9ae..8f4e470f 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Mini Timeline", + "format": { + "editor_label": "Time & date format", + "24h": "Use 24-hour clock" + }, "mode": "Mode", "modes": { "above": "Above", diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json index ae8ac97f..a340fdab 100644 --- a/src/localize/languages/fr.json +++ b/src/localize/languages/fr.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Mini-chronologie", + "format": { + "24h": "", + "editor_label": "" + }, "mode": "Mode", "modes": { "above": "Au-dessus", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index 861a8936..53fafff1 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Mini Cronologia", + "format": { + "24h": "", + "editor_label": "" + }, "mode": "Modalità", "modes": { "above": "sopra", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index d153164e..973efc45 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Controles da linha do tempo", + "format": { + "24h": "", + "editor_label": "" + }, "mode": "Modo", "modes": { "above": "Acima", diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index 04fb2152..cdc2b9a9 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -231,6 +231,10 @@ }, "timeline": { "editor_label": "Controles de linha do tempo", + "format": { + "24h": "", + "editor_label": "" + }, "mode": "Modo", "modes": { "above": "Por cima", diff --git a/tests/config/types.test.ts b/tests/config/types.test.ts index f9cbc502..cbb9964b 100644 --- a/tests/config/types.test.ts +++ b/tests/config/types.test.ts @@ -105,6 +105,9 @@ describe('config defaults', () => { timeline: { clustering_threshold: 3, events_media_type: 'all', + format: { + '24h': true, + }, mode: 'none', pan_mode: 'pan', show_recordings: true, @@ -170,6 +173,9 @@ describe('config defaults', () => { timeline: { clustering_threshold: 3, events_media_type: 'all', + format: { + '24h': true, + }, mode: 'none', pan_mode: 'pan', show_recordings: true, @@ -324,6 +330,9 @@ describe('config defaults', () => { }, }, events_media_type: 'all', + format: { + '24h': true, + }, pan_mode: 'pan', show_recordings: true, style: 'stack',