From 91f6f48696f0afad3502a56a548ee285cdbb4ca6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 15 Apr 2023 09:31:05 -0700 Subject: [PATCH] Add label to timeline marker with current time. --- src/components/timeline-core.ts | 35 ++++++++++++++++++++++++--------- src/scss/timeline-core.scss | 25 +++++++++++++++++++---- src/utils/basic.ts | 4 ++-- tests/basic.test.ts | 20 +++++++++++++++++++ 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts index 73e6bd6a..475ec1c9 100644 --- a/src/components/timeline-core.ts +++ b/src/components/timeline-core.ts @@ -19,7 +19,7 @@ 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, IdType } from 'vis-timeline/esnext'; +import type { DataGroupCollectionType, DateType, IdType } from 'vis-timeline/esnext'; import { Timeline, TimelineEventPropertiesResult, @@ -46,6 +46,7 @@ import { stopEventFromActivatingCardWideActions } from '../utils/action'; import { contentsChanged, dispatchFrigateCardEvent, + formatDateAndTime, isHoverableDevice, setOrRemoveAttribute, } from '../utils/basic'; @@ -89,6 +90,11 @@ declare module 'view' { } } +interface ExtendedTimeline extends Timeline { + // setCustomTimeMarker currently missing from Timeline types. + setCustomTimeMarker?(time: DateType, id?: IdType): void; +} + // An event used to fetch data required for thumbnail rendering. See special // note below on why this is necessary. interface ThumbnailDataRequest { @@ -206,8 +212,7 @@ export class FrigateCardTimelineCore extends LitElement { protected _refDatePicker: Ref = createRef(); protected _refTimeline: Ref = createRef(); - - protected _timeline?: Timeline; + protected _timeline?: ExtendedTimeline; protected _timelineSource: TimelineDataSource | null = null; @@ -417,6 +422,22 @@ export class FrigateCardTimelineCore extends LitElement { } else { this._timeline?.setCustomTime(targetTime, TIMELINE_TARGET_BAR_ID); } + + const window = this._timeline.getWindow(); + const markerProportion = + (targetTime.getTime() - window.start.getTime()) / + (window.end.getTime() - window.start.getTime()); + + // Position the marker proportionally to how 'far' the pointer is being + // held relative to the timeline window. + this.setAttribute( + 'target-bar-marker-direction', + markerProportion < 0.25 ? 'right' : markerProportion > 0.75 ? 'left' : 'center', + ); + this._timeline?.setCustomTimeMarker?.( + formatDateAndTime(targetTime, true), + TIMELINE_TARGET_BAR_ID, + ); } else { this._removeTargetBar(); } @@ -426,6 +447,7 @@ export class FrigateCardTimelineCore extends LitElement { * Remove the target bar. */ protected _removeTargetBar(): void { + this.removeAttribute('target-bar-direction'); if (this._targetBarVisible) { this._timeline?.removeCustomTime(TIMELINE_TARGET_BAR_ID); this._targetBarVisible = false; @@ -463,12 +485,7 @@ export class FrigateCardTimelineCore extends LitElement { : results .clone() .resetSelectedResult() - .selectBestResult((media) => - findBestMediaIndex( - media, - targetTime, - ), - ); + .selectBestResult((media) => findBestMediaIndex(media, targetTime)); const desiredView: FrigateCardView = this.mini ? targetTime >= new Date() diff --git a/src/scss/timeline-core.scss b/src/scss/timeline-core.scss index 3079bc1c..9a9403c5 100644 --- a/src/scss/timeline-core.scss +++ b/src/scss/timeline-core.scss @@ -144,13 +144,30 @@ div.vis-tooltip { } .target_bar { - border-left: 2px solid var(--primary-color); - opacity: 0.7; - box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 3px 1px var(--primary-color)); + background-color: var(--primary-color); + width: 2px; + + z-index: 20; // Prevent the mouse interacting with the custom time. pointer-events: none; } +.target_bar .vis-custom-time-marker { + background-color: var(--primary-background-color); + color: var(--primary-text-color); + bottom: 0px; + top: unset; +} +:host([target-bar-marker-direction='right']) .target_bar .vis-custom-time-marker { + left: 2px; +} +:host([target-bar-marker-direction='left']) .target_bar .vis-custom-time-marker { + right: 2px; +} +:host([target-bar-marker-direction='center']) .target_bar .vis-custom-time-marker { + left: 0px; + transform: translateX(-50%); +} .timeline-tools { position: absolute; @@ -162,4 +179,4 @@ div.vis-tooltip { .timeline-tools ha-icon { cursor: pointer; -} \ No newline at end of file +} diff --git a/src/utils/basic.ts b/src/utils/basic.ts index 5ca9738e..ac4b9a2d 100644 --- a/src/utils/basic.ts +++ b/src/utils/basic.ts @@ -114,8 +114,8 @@ export const isHoverableDevice = (): boolean => * @param date A Date object. * @returns A date and time. */ -export const formatDateAndTime = (date: Date): string => { - return format(date, 'yyyy-MM-dd HH:mm'); +export const formatDateAndTime = (date: Date, includeSeconds?: boolean): string => { + return format(date, `yyyy-MM-dd HH:mm${includeSeconds ? ':ss' : ''}`); }; /** diff --git a/tests/basic.test.ts b/tests/basic.test.ts index fa753005..3ae271f7 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -17,6 +17,7 @@ import { prettifyTitle, runWhenIdleIfSupported, setify, + setOrRemoveAttribute, sleep, } from '../src/utils/basic'; @@ -131,6 +132,10 @@ describe('formatDateAndTime', () => { const date = new Date(2023, 3, 14, 13, 35, 0); expect(formatDateAndTime(date)).toBe('2023-04-14 13:35'); }); + it('should format date and time with seconds', () => { + const date = new Date(2023, 3, 14, 13, 35, 1); + expect(formatDateAndTime(date, true)).toBe('2023-04-14 13:35:01'); + }); }); describe('formatDate', () => { @@ -220,3 +225,18 @@ describe('isValidDate', () => { expect(isValidDate(new Date('moo'))).toBeFalsy(); }); }); + +describe('setOrRemoveAttribute', () => { + it('should set attribute', () => { + const element = document.createElement('div'); + setOrRemoveAttribute(element, true, 'key', 'value'); + expect(element.getAttribute('key')).toBe('value'); + }); + + it('should remove attribute date', () => { + const element = document.createElement('div'); + element.setAttribute('key', 'value'); + setOrRemoveAttribute(element, false, 'key'); + expect(element.getAttribute('key')).toBeFalsy(); + }); +});