diff --git a/src/components/drawer.ts b/src/components/drawer.ts
index 4f3986de..fff6936c 100644
--- a/src/components/drawer.ts
+++ b/src/components/drawer.ts
@@ -18,6 +18,9 @@ export class FrigateCardDrawer extends LitElement {
@property({ attribute: true, reflect: true })
public location: 'left' | 'right' = 'left';
+ @property({ attribute: true, reflect: true, type: Boolean })
+ public control = true;
+
/**
* Set the timeline configuration.
*/
@@ -52,9 +55,27 @@ export class FrigateCardDrawer extends LitElement {
}
protected render(): TemplateResult {
- return html`
-
- `;
+ return html`
+
+ ${this.control
+ ? html`
+ {
+ this.open = !this.open;
+ }}
+ >
+
+
+
+ `
+ : ''}
+
+
+ `;
}
static get styles(): CSSResultGroup {
diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts
index e21a50e5..837b10fd 100644
--- a/src/components/thumbnail-carousel.ts
+++ b/src/components/thumbnail-carousel.ts
@@ -1,11 +1,13 @@
import { BrowseMediaUtil } from '../browse-media-util.js';
import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit';
import { EmblaOptionsType } from 'embla-carousel';
+import { classMap } from 'lit/directives/class-map.js';
import { customElement, property } from 'lit/decorators.js';
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
import { FrigateCardCarousel } from './carousel.js';
import {
+ contentsChanged,
dispatchFrigateCardEvent,
stopEventFromActivatingCardWideActions,
} from '../common.js';
@@ -20,10 +22,11 @@ export interface ThumbnailCarouselTap {
@customElement('frigate-card-thumbnail-carousel')
export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
- @property({ attribute: false })
+ @property({ attribute: false, hasChanged: contentsChanged })
public target?: FrigateBrowseMediaSource;
- protected _tapSelected?;
+ @property({ attribute: false, reflect: true })
+ public selected?: number | null;
@property({ attribute: false })
set config(config: ThumbnailsControlConfig | undefined) {
@@ -55,25 +58,6 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
};
}
- /**
- * Scroll to a particular slide.
- * @param index Slide number.
- */
- carouselScrollTo(index: number): void {
- if (!this._carousel) {
- return;
- }
-
- if (this._tapSelected !== undefined) {
- this._carousel.slideNodes()[this._tapSelected].classList.remove('slide-selected');
- }
-
- super.carouselScrollTo(index);
-
- this._carousel.slideNodes()[index].classList.add('slide-selected');
- this._tapSelected = index;
- }
-
/**
* Get slides to include in the render.
* @returns The slides to include in the render.
@@ -98,10 +82,20 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
* @param changedProperties The properties that were changed in this render.
*/
updated(changedProperties: PropertyValues): void {
- if (this._carousel && changedProperties.has('target')) {
+ if (changedProperties.has('target')) {
this._destroyCarousel();
}
super.updated(changedProperties);
+
+ if (changedProperties.has('selected')) {
+ this.updateComplete.then(() => {
+ if (this._carousel) {
+ if (this.selected !== undefined && this.selected !== null) {
+ this.carouselScrollTo(this.selected);
+ }
+ }
+ });
+ }
}
/**
@@ -123,8 +117,13 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
return;
}
+ const classes = {
+ 'embla__slide': true,
+ 'slide-selected': this.selected == childIndex,
+ };
+
return html`
{
if (this._carousel && this._carousel.clickAllowed()) {
dispatchFrigateCardEvent
(this, 'carousel:tap', {
diff --git a/src/components/timeline.ts b/src/components/timeline.ts
index 6aa0ef67..3305b84d 100644
--- a/src/components/timeline.ts
+++ b/src/components/timeline.ts
@@ -14,6 +14,7 @@ import {
TimelineOptions,
TimelineOptionsCluster,
} from 'vis-timeline/esnext';
+import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
@@ -22,12 +23,18 @@ import {
CameraConfig,
ExtendedHomeAssistant,
FrigateBrowseMediaSource,
+ ThumbnailsControlConfig,
MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_VIDEO,
MEDIA_CLASS_VIDEO,
TimelineConfig,
frigateCardConfigDefaults,
} from '../types';
+import { FrigateCardDrawer } from './drawer';
+import {
+ FrigateCardThumbnailCarousel,
+ ThumbnailCarouselTap,
+} from './thumbnail-carousel';
import { View } from '../view';
import {
contentsChanged,
@@ -39,6 +46,8 @@ import {
import timelineStyle from '../scss/timeline.scss';
import timelineEventStyle from '../scss/timeline-event.scss';
+import './drawer.js';
+
interface FrigateCardGroupData {
id: string;
content: string;
@@ -217,7 +226,9 @@ export class FrigateCardTimeline extends LitElement {
@state({ hasChanged: contentsChanged })
protected _timelineOptions?: TimelineOptions;
+ protected _drawerRef: Ref = createRef();
protected _timelineRef: Ref = createRef();
+ protected _thumbnailsRef: Ref = createRef();
protected _timeline?: Timeline;
protected _events = new TimelineEventManager({
@@ -251,7 +262,41 @@ export class FrigateCardTimeline extends LitElement {
if (!this.hass || !this.view) {
return;
}
- return html` `;
+
+ const config: ThumbnailsControlConfig = {
+ mode: 'above',
+ };
+
+ // TODO move to configuration later.
+ const drawerLocation: "left" | "right" = "left" as const;
+
+ const timelineClasses = {
+ "timeline": true,
+ "left-margin": drawerLocation == "left",
+ //"right-margin": drawerLocation == "right",
+ }
+
+ return html`
+ ) => {
+ if (ev.detail.target && ev.detail.childIndex) {
+ this.view
+ ?.evolve({
+ target: ev.detail.target,
+ childIndex: ev.detail.childIndex,
+ view: 'clip',
+ })
+ .dispatchChangeEvent(this);
+ }
+ }}
+ >
+
+
+ `;
}
/**
@@ -290,7 +335,9 @@ export class FrigateCardTimeline extends LitElement {
this.cameras,
properties.start,
properties.end,
- );
+ ).then(() => {
+ this._updateThumbnails();
+ })
}
}
@@ -299,11 +346,21 @@ export class FrigateCardTimeline extends LitElement {
* @param data The data about the selection.
* @returns
*/
- protected _timelineSelectHandler(data: { items: string[]; event: Event }): void {
- if (data.items.length <= 0 || !this._timeline) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ protected _timelineSelectHandler(_data: { items: string[]; event: Event }): void {
+ this._updateThumbnails();
+ if (this._drawerRef.value) {
+ this._drawerRef.value.open = true;
+ }
+ }
+
+ protected _updateThumbnails(): void {
+ if (!this._timeline) {
return;
}
+ const selected = this._timeline?.getSelection();
+
const timelineWindow = this._timeline.getWindow();
const start = timelineWindow.start.getTime();
const end = timelineWindow.end.getTime();
@@ -316,12 +373,17 @@ export class FrigateCardTimeline extends LitElement {
this._events.dataset
.get({
filter: (item) =>
+ // Start within the window.
(item.start >= start && item.start <= end) ||
+ // End within the window.
+ (!!item.end && item.end >= start && item.end <= end) ||
+ // Item lifetime extends past the window
(item.start <= start && !!item.end && item.end >= end),
+ order: 'start',
})
.forEach((item) => {
if (item.source.can_play) {
- if ((item.id = data.items[0])) {
+ if (childIndex === null && selected.includes(item.id)) {
childIndex = children.length;
}
children.push(item.source);
@@ -332,23 +394,24 @@ export class FrigateCardTimeline extends LitElement {
return;
}
- this.view
- ?.evolve({
- target: {
- title: `Timeline ${start} - ${end}`,
- media_class: MEDIA_CLASS_PLAYLIST,
- media_content_type: MEDIA_TYPE_VIDEO,
- media_content_id: '',
- can_play: false,
- can_expand: true,
- children_media_class: MEDIA_CLASS_VIDEO,
- thumbnail: null,
- children: children,
- },
- childIndex: childIndex ?? 0,
- view: 'clip',
- })
- .dispatchChangeEvent(this);
+ const target = {
+ title: `Timeline ${start} - ${end}`,
+ media_class: MEDIA_CLASS_PLAYLIST,
+ media_content_type: MEDIA_TYPE_VIDEO,
+ media_content_id: '',
+ can_play: false,
+ can_expand: true,
+ children_media_class: MEDIA_CLASS_VIDEO,
+ thumbnail: null,
+ children: children,
+ };
+
+ if (this._drawerRef.value) {
+ if (this._thumbnailsRef.value) {
+ this._thumbnailsRef.value.target = target;
+ this._thumbnailsRef.value.selected = childIndex ?? undefined;
+ }
+ }
}
/**
@@ -384,6 +447,7 @@ export class FrigateCardTimeline extends LitElement {
cluster:
thumbnailConfig.clustering_threshold > 0
? {
+ fitOnDoubleClick: true,
showStipes: true,
// It would be better to automatically calculate `maxItems` from the
// rendered height of the timeline (or group within the timeline) so
@@ -404,6 +468,7 @@ export class FrigateCardTimeline extends LitElement {
},
zoomMax: 31 * 24 * 60 * 60 * 1000,
zoomMin: 1 * 1000,
+ selectable: true,
start: this._getYesterday(),
end: this._getToday(),
groupHeightMode: 'fixed',
diff --git a/src/components/viewer.ts b/src/components/viewer.ts
index a5aa231f..d0aa7754 100644
--- a/src/components/viewer.ts
+++ b/src/components/viewer.ts
@@ -135,8 +135,8 @@ export class FrigateCardViewerCore extends LitElement {
protected _syncThumbnailCarousel(): void {
const mediaSelected = this._viewerCarouselRef.value?.carouselSelected();
- if (mediaSelected !== undefined) {
- this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected);
+ if (mediaSelected !== undefined && this._thumbnailCarouselRef.value) {
+ this._thumbnailCarouselRef.value.selected = mediaSelected;
}
}
diff --git a/src/scss/drawer-inject.scss b/src/scss/drawer-inject.scss
index 30b060ac..2033531f 100644
--- a/src/scss/drawer-inject.scss
+++ b/src/scss/drawer-inject.scss
@@ -1,34 +1,43 @@
:host {
- // Drawer width sizes to contents.
- width: unset;
-};
-
-#d, #fs {
- // Override width/height to be 100% instead of 100v[wh].
- height: 100%;
-
- // Position absolutely.
- position: absolute;
-}
-
-:host([location=right]) #d {
- // Position to the right.
- left: unset;
- right: 0;
- transform: translateX(100%);
-};
-
-:host([location=right][open]) #d {
- transform: none;
- box-shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.5);
+ // Drawer width sizes to contents.
+ width: unset;
}
#fs {
- width: 100%;
- inset: 0;
+ // Hide the freespace screen.
+ display: none;
+ width: 100%;
+ inset: 0;
+}
+
+#d,
+#fs {
+ // Override width/height to be 100% instead of 100v[wh].
+ height: 100%;
+
+ // Position absolutely.
+ position: absolute;
+}
+
+#d {
+ // Adding for control, may not need this?
+ overflow: visible;
+ visibility: visible;
+}
+
+:host([location='right']) #d {
+ // Position to the right.
+ left: unset;
+ right: 0;
+ transform: translateX(100%);
+}
+
+:host([location='right'][open]) #d {
+ transform: none;
+ box-shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.5);
}
#ifs {
- // Override width/height to be 100% instead of 100v[wh].
- height: 100%;
-};
\ No newline at end of file
+ // Override width/height to be 100% instead of 100v[wh].
+ height: 100%;
+}
diff --git a/src/scss/drawer.scss b/src/scss/drawer.scss
index 3d23bb67..3ed79a2f 100644
--- a/src/scss/drawer.scss
+++ b/src/scss/drawer.scss
@@ -1,3 +1,49 @@
+$drawer-icon-size: 20px;
+$drawer-padding-extend: 20px;
+
side-drawer {
- background-color: var(--card-background-color);
+ background-color: var(--card-background-color);
+}
+
+div.control-surround {
+ position: absolute;
+ bottom: 50%;
+ z-index: 0;
+ padding-top: $drawer-padding-extend;
+ padding-bottom: $drawer-padding-extend;
+}
+:host([location='left']) div.control-surround {
+ @if $drawer-icon-size < 32 {
+ // Ensure the clickable area is at least 32px wide.
+ padding-right: calc(32px - $drawer-icon-size);
+ }
+ left: 100%;
+}
+:host([location='right']) div.control-surround {
+ @if $drawer-icon-size < 32 {
+ // See note above.
+ padding-left: calc(32px - $drawer-icon-size);
+ }
+ right: 100%;
+}
+
+ha-icon.control {
+ color: var(--secondary-color, white);
+ background-color: rgba(0, 0, 0, 0.6);
+ opacity: 0.6;
+ pointer-events: all;
+
+ --mdc-icon-size: #{$drawer-icon-size};
+ padding-top: $drawer-padding-extend;
+ padding-bottom: $drawer-padding-extend;
+}
+
+:host([location='left']) ha-icon.control {
+ border-top-right-radius: $drawer-icon-size;
+ border-bottom-right-radius: $drawer-icon-size;
+}
+
+:host([location='right']) ha-icon.control {
+ border-top-left-radius: $drawer-icon-size;
+ border-bottom-left-radius: $drawer-icon-size;
}
diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss
index ff274ecc..fe15c743 100644
--- a/src/scss/timeline.scss
+++ b/src/scss/timeline.scss
@@ -1,4 +1,5 @@
@use 'vis-timeline/dist/vis-timeline-graph2d.css';
+@use 'drawer';
:host {
width: 100%;
@@ -14,6 +15,14 @@
div.timeline {
height: 100%;
}
+div.timeline.left-margin {
+ // Clearance for the drawer button.
+ margin-left: drawer.$drawer-icon-size;
+}
+div.timeline.right-margin {
+ // Clearance for the drawer button.
+ margin-right: drawer.$drawer-icon-size;
+}
.vis-text {
color: var(--primary-text-color) !important;