Improve drawer controls.

This commit is contained in:
Dermot Duffy
2022-03-20 18:07:48 -07:00
parent 304abcd82b
commit 50596cb68a
7 changed files with 227 additions and 78 deletions
+23 -2
View File
@@ -18,6 +18,9 @@ export class FrigateCardDrawer extends LitElement {
@property({ attribute: true, reflect: true }) @property({ attribute: true, reflect: true })
public location: 'left' | 'right' = 'left'; public location: 'left' | 'right' = 'left';
@property({ attribute: true, reflect: true, type: Boolean })
public control = true;
/** /**
* Set the timeline configuration. * Set the timeline configuration.
*/ */
@@ -52,9 +55,27 @@ export class FrigateCardDrawer extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
return html` <side-drawer location="${this.location}" ${ref(this._drawerRef)}> return html`
<side-drawer location="${this.location}" ${ref(this._drawerRef)}>
${this.control
? html`
<div
class="control-surround"
@click=${() => {
this.open = !this.open;
}}
>
<ha-icon
class="control"
icon="${this.open ? 'mdi:menu' : 'mdi:menu-open'}"
>
</ha-icon>
</div>
`
: ''}
<slot></slot> <slot></slot>
</side-drawer>`; </side-drawer>
`;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
+22 -23
View File
@@ -1,11 +1,13 @@
import { BrowseMediaUtil } from '../browse-media-util.js'; import { BrowseMediaUtil } from '../browse-media-util.js';
import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit';
import { EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType } from 'embla-carousel';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
import { FrigateCardCarousel } from './carousel.js'; import { FrigateCardCarousel } from './carousel.js';
import { import {
contentsChanged,
dispatchFrigateCardEvent, dispatchFrigateCardEvent,
stopEventFromActivatingCardWideActions, stopEventFromActivatingCardWideActions,
} from '../common.js'; } from '../common.js';
@@ -20,10 +22,11 @@ export interface ThumbnailCarouselTap {
@customElement('frigate-card-thumbnail-carousel') @customElement('frigate-card-thumbnail-carousel')
export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
@property({ attribute: false }) @property({ attribute: false, hasChanged: contentsChanged })
public target?: FrigateBrowseMediaSource; public target?: FrigateBrowseMediaSource;
protected _tapSelected?; @property({ attribute: false, reflect: true })
public selected?: number | null;
@property({ attribute: false }) @property({ attribute: false })
set config(config: ThumbnailsControlConfig | undefined) { 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. * Get slides to include in the render.
* @returns The 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. * @param changedProperties The properties that were changed in this render.
*/ */
updated(changedProperties: PropertyValues): void { updated(changedProperties: PropertyValues): void {
if (this._carousel && changedProperties.has('target')) { if (changedProperties.has('target')) {
this._destroyCarousel(); this._destroyCarousel();
} }
super.updated(changedProperties); 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; return;
} }
const classes = {
'embla__slide': true,
'slide-selected': this.selected == childIndex,
};
return html`<div return html`<div
class="embla__slide" class="${classMap(classes)}"
@click=${(ev) => { @click=${(ev) => {
if (this._carousel && this._carousel.clickAllowed()) { if (this._carousel && this._carousel.clickAllowed()) {
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', { dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
+78 -13
View File
@@ -14,6 +14,7 @@ import {
TimelineOptions, TimelineOptions,
TimelineOptionsCluster, TimelineOptionsCluster,
} from 'vis-timeline/esnext'; } from 'vis-timeline/esnext';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js';
@@ -22,12 +23,18 @@ import {
CameraConfig, CameraConfig,
ExtendedHomeAssistant, ExtendedHomeAssistant,
FrigateBrowseMediaSource, FrigateBrowseMediaSource,
ThumbnailsControlConfig,
MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO,
MEDIA_CLASS_VIDEO, MEDIA_CLASS_VIDEO,
TimelineConfig, TimelineConfig,
frigateCardConfigDefaults, frigateCardConfigDefaults,
} from '../types'; } from '../types';
import { FrigateCardDrawer } from './drawer';
import {
FrigateCardThumbnailCarousel,
ThumbnailCarouselTap,
} from './thumbnail-carousel';
import { View } from '../view'; import { View } from '../view';
import { import {
contentsChanged, contentsChanged,
@@ -39,6 +46,8 @@ import {
import timelineStyle from '../scss/timeline.scss'; import timelineStyle from '../scss/timeline.scss';
import timelineEventStyle from '../scss/timeline-event.scss'; import timelineEventStyle from '../scss/timeline-event.scss';
import './drawer.js';
interface FrigateCardGroupData { interface FrigateCardGroupData {
id: string; id: string;
content: string; content: string;
@@ -217,7 +226,9 @@ export class FrigateCardTimeline extends LitElement {
@state({ hasChanged: contentsChanged }) @state({ hasChanged: contentsChanged })
protected _timelineOptions?: TimelineOptions; protected _timelineOptions?: TimelineOptions;
protected _drawerRef: Ref<FrigateCardDrawer> = createRef();
protected _timelineRef: Ref<HTMLElement> = createRef(); protected _timelineRef: Ref<HTMLElement> = createRef();
protected _thumbnailsRef: Ref<FrigateCardThumbnailCarousel> = createRef();
protected _timeline?: Timeline; protected _timeline?: Timeline;
protected _events = new TimelineEventManager({ protected _events = new TimelineEventManager({
@@ -251,7 +262,41 @@ export class FrigateCardTimeline extends LitElement {
if (!this.hass || !this.view) { if (!this.hass || !this.view) {
return; return;
} }
return html` <div class="timeline" ${ref(this._timelineRef)}></div>`;
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` <frigate-card-drawer location="${drawerLocation}" ${ref(this._drawerRef)}>
<frigate-card-thumbnail-carousel
${ref(this._thumbnailsRef)}
direction="vertical"
.config=${config}
.highlightSelected=${true}
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
if (ev.detail.target && ev.detail.childIndex) {
this.view
?.evolve({
target: ev.detail.target,
childIndex: ev.detail.childIndex,
view: 'clip',
})
.dispatchChangeEvent(this);
}
}}
>
</frigate-card-thumbnail-carousel>
</frigate-card-drawer>
<div class="${classMap(timelineClasses)}" ${ref(this._timelineRef)}></div>`;
} }
/** /**
@@ -290,7 +335,9 @@ export class FrigateCardTimeline extends LitElement {
this.cameras, this.cameras,
properties.start, properties.start,
properties.end, properties.end,
); ).then(() => {
this._updateThumbnails();
})
} }
} }
@@ -299,11 +346,21 @@ export class FrigateCardTimeline extends LitElement {
* @param data The data about the selection. * @param data The data about the selection.
* @returns * @returns
*/ */
protected _timelineSelectHandler(data: { items: string[]; event: Event }): void { // eslint-disable-next-line @typescript-eslint/no-unused-vars
if (data.items.length <= 0 || !this._timeline) { 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; return;
} }
const selected = this._timeline?.getSelection();
const timelineWindow = this._timeline.getWindow(); const timelineWindow = this._timeline.getWindow();
const start = timelineWindow.start.getTime(); const start = timelineWindow.start.getTime();
const end = timelineWindow.end.getTime(); const end = timelineWindow.end.getTime();
@@ -316,12 +373,17 @@ export class FrigateCardTimeline extends LitElement {
this._events.dataset this._events.dataset
.get({ .get({
filter: (item) => filter: (item) =>
// Start within the window.
(item.start >= start && item.start <= end) || (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), (item.start <= start && !!item.end && item.end >= end),
order: 'start',
}) })
.forEach((item) => { .forEach((item) => {
if (item.source.can_play) { if (item.source.can_play) {
if ((item.id = data.items[0])) { if (childIndex === null && selected.includes(item.id)) {
childIndex = children.length; childIndex = children.length;
} }
children.push(item.source); children.push(item.source);
@@ -332,9 +394,7 @@ export class FrigateCardTimeline extends LitElement {
return; return;
} }
this.view const target = {
?.evolve({
target: {
title: `Timeline ${start} - ${end}`, title: `Timeline ${start} - ${end}`,
media_class: MEDIA_CLASS_PLAYLIST, media_class: MEDIA_CLASS_PLAYLIST,
media_content_type: MEDIA_TYPE_VIDEO, media_content_type: MEDIA_TYPE_VIDEO,
@@ -344,11 +404,14 @@ export class FrigateCardTimeline extends LitElement {
children_media_class: MEDIA_CLASS_VIDEO, children_media_class: MEDIA_CLASS_VIDEO,
thumbnail: null, thumbnail: null,
children: children, children: children,
}, };
childIndex: childIndex ?? 0,
view: 'clip', if (this._drawerRef.value) {
}) if (this._thumbnailsRef.value) {
.dispatchChangeEvent(this); this._thumbnailsRef.value.target = target;
this._thumbnailsRef.value.selected = childIndex ?? undefined;
}
}
} }
/** /**
@@ -384,6 +447,7 @@ export class FrigateCardTimeline extends LitElement {
cluster: cluster:
thumbnailConfig.clustering_threshold > 0 thumbnailConfig.clustering_threshold > 0
? { ? {
fitOnDoubleClick: true,
showStipes: true, showStipes: true,
// It would be better to automatically calculate `maxItems` from the // It would be better to automatically calculate `maxItems` from the
// rendered height of the timeline (or group within the timeline) so // 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, zoomMax: 31 * 24 * 60 * 60 * 1000,
zoomMin: 1 * 1000, zoomMin: 1 * 1000,
selectable: true,
start: this._getYesterday(), start: this._getYesterday(),
end: this._getToday(), end: this._getToday(),
groupHeightMode: 'fixed', groupHeightMode: 'fixed',
+2 -2
View File
@@ -135,8 +135,8 @@ export class FrigateCardViewerCore extends LitElement {
protected _syncThumbnailCarousel(): void { protected _syncThumbnailCarousel(): void {
const mediaSelected = this._viewerCarouselRef.value?.carouselSelected(); const mediaSelected = this._viewerCarouselRef.value?.carouselSelected();
if (mediaSelected !== undefined) { if (mediaSelected !== undefined && this._thumbnailCarouselRef.value) {
this._thumbnailCarouselRef.value?.carouselScrollTo(mediaSelected); this._thumbnailCarouselRef.value.selected = mediaSelected;
} }
} }
+21 -12
View File
@@ -1,9 +1,17 @@
:host { :host {
// Drawer width sizes to contents. // Drawer width sizes to contents.
width: unset; width: unset;
}; }
#d, #fs { #fs {
// Hide the freespace screen.
display: none;
width: 100%;
inset: 0;
}
#d,
#fs {
// Override width/height to be 100% instead of 100v[wh]. // Override width/height to be 100% instead of 100v[wh].
height: 100%; height: 100%;
@@ -11,24 +19,25 @@
position: absolute; position: absolute;
} }
:host([location=right]) #d { #d {
// Adding for control, may not need this?
overflow: visible;
visibility: visible;
}
:host([location='right']) #d {
// Position to the right. // Position to the right.
left: unset; left: unset;
right: 0; right: 0;
transform: translateX(100%); transform: translateX(100%);
};
:host([location=right][open]) #d {
transform: none;
box-shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.5);
} }
#fs { :host([location='right'][open]) #d {
width: 100%; transform: none;
inset: 0; box-shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.5);
} }
#ifs { #ifs {
// Override width/height to be 100% instead of 100v[wh]. // Override width/height to be 100% instead of 100v[wh].
height: 100%; height: 100%;
}; }
+46
View File
@@ -1,3 +1,49 @@
$drawer-icon-size: 20px;
$drawer-padding-extend: 20px;
side-drawer { 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;
}
+9
View File
@@ -1,4 +1,5 @@
@use 'vis-timeline/dist/vis-timeline-graph2d.css'; @use 'vis-timeline/dist/vis-timeline-graph2d.css';
@use 'drawer';
:host { :host {
width: 100%; width: 100%;
@@ -14,6 +15,14 @@
div.timeline { div.timeline {
height: 100%; 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 { .vis-text {
color: var(--primary-text-color) !important; color: var(--primary-text-color) !important;