diff --git a/package.json b/package.json index b1824898..6d8a11b6 100644 --- a/package.json +++ b/package.json @@ -24,16 +24,17 @@ "@types/bluebird": "^3.5.36", "component-emitter": "^1.3.0", "crypto": "^1.0.1", - "custom-card-helpers": "^1.8.0", + "custom-card-helpers": "^1.9.0", "embla-carousel": "^6.1.1", "home-assistant-js-websocket": "^6.1.1", - "lit": "^2.2.1", "keycharm": "^0.4.0", + "lit": "^2.2.1", "lodash-es": "^4.17.21", "moment": "^2.29.1", "propagating-hammerjs": "^2.0.1", "quick-lru": "^6.1.0", "screenfull": "^6.0.1", + "side-drawer": "^3.0.0", "ts-toolbelt": "^9.6.0", "uuid": "^8.3.2", "vis-data": "^7.1.3", diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 217506d9..85236657 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,4 +1,6 @@ import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; +import { property } from 'lit/decorators.js'; + import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType, @@ -15,6 +17,9 @@ export interface CarouselSelect { } export class FrigateCardCarousel extends LitElement { + @property({ attribute: true, reflect: true }) + public direction: 'vertical' | 'horizontal' = 'horizontal'; + protected _carousel?: EmblaCarouselType; protected _plugins: Record = {}; @@ -98,7 +103,13 @@ export class FrigateCardCarousel extends LitElement { return acc; }, {}); - this._carousel = EmblaCarousel(carouselNode, this._getOptions(), plugins); + this._carousel = EmblaCarousel( + carouselNode, + { + axis: this.direction == 'horizontal' ? 'x' : 'y', + ...this._getOptions() + }, + plugins); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('select', () => { const selected = this.carouselSelected(); diff --git a/src/components/drawer.ts b/src/components/drawer.ts new file mode 100644 index 00000000..4f3986de --- /dev/null +++ b/src/components/drawer.ts @@ -0,0 +1,63 @@ +import { + CSSResultGroup, + LitElement, + TemplateResult, + html, + unsafeCSS, + PropertyValues, +} from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { createRef, ref, Ref } from 'lit/directives/ref.js'; +import 'side-drawer'; + +import drawerStyle from '../scss/drawer.scss'; +import drawerInjectStyle from '../scss/drawer-inject.scss'; + +@customElement('frigate-card-drawer') +export class FrigateCardDrawer extends LitElement { + @property({ attribute: true, reflect: true }) + public location: 'left' | 'right' = 'left'; + + /** + * Set the timeline configuration. + */ + @property({ type: Boolean, reflect: true, attribute: true }) + set open(open: boolean) { + if (this._drawerRef.value) { + const old = this._drawerRef.value.open; + this._drawerRef.value.open = open; + this.requestUpdate('open', old); + } + } + + get open(): boolean { + return this._drawerRef.value?.open ?? false; + } + + protected _drawerRef: Ref = createRef(); + + /** + * Called on the first update. + * @param changedProps The changed properties. + */ + protected firstUpdated(changedProps: PropertyValues): void { + super.firstUpdated(changedProps); + + // The `side-drawer` component (and the material drawer for that matter) + // only do fixed drawers (i.e. a drawer for the whole viewport). Hackily + // override the style to customize the drawer to be absolute within the div. + const style = document.createElement('style'); + style.innerHTML = drawerInjectStyle; + this._drawerRef.value?.shadowRoot?.appendChild(style); + } + + protected render(): TemplateResult { + return html` + + `; + } + + static get styles(): CSSResultGroup { + return unsafeCSS(drawerStyle); + } +} diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index 3110a020..e21a50e5 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -1,11 +1,14 @@ import { BrowseMediaUtil } from '../browse-media-util.js'; -import { CSSResultGroup, TemplateResult, html, unsafeCSS } from 'lit'; +import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { EmblaOptionsType } from 'embla-carousel'; import { customElement, property } from 'lit/decorators.js'; import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; import { FrigateCardCarousel } from './carousel.js'; -import { dispatchFrigateCardEvent, stopEventFromActivatingCardWideActions } from '../common.js'; +import { + dispatchFrigateCardEvent, + stopEventFromActivatingCardWideActions, +} from '../common.js'; import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; @@ -18,7 +21,7 @@ export interface ThumbnailCarouselTap { @customElement('frigate-card-thumbnail-carousel') export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { @property({ attribute: false }) - protected target?: FrigateBrowseMediaSource; + public target?: FrigateBrowseMediaSource; protected _tapSelected?; @@ -90,6 +93,17 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { return slides; } + /** + * The updated lifecycle callback for this element. + * @param changedProperties The properties that were changed in this render. + */ + updated(changedProperties: PropertyValues): void { + if (this._carousel && changedProperties.has('target')) { + this._destroyCarousel(); + } + super.updated(changedProperties); + } + /** * Render a given thumbnail. * @param mediaToRender The media item to render. diff --git a/src/scss/carousel.scss b/src/scss/carousel.scss index 68a2132e..46c973c2 100644 --- a/src/scss/carousel.scss +++ b/src/scss/carousel.scss @@ -23,11 +23,19 @@ img,video { width: 100%; height: 100%; + flex-direction: column; + user-select: none; -webkit-touch-callout: none; -khtml-user-select: none; -webkit-tap-highlight-color: transparent; } +:host([direction="vertical"]) .embla__container { + flex-direction: column; +} +:host([direction="horizontal"]) .embla__container { + flex-direction: row; +} .embla__viewport { width: 100%; @@ -50,9 +58,14 @@ img,video { .embla__slide { position: relative; height: 100%; - margin-right: 5px; overflow: visible; } +:host([direction="vertical"]) .embla__slide { + margin-bottom: 5px; +} +:host([direction="horizontal"]) .embla__slide { + margin-right: 5px; +} .embla__slide img,video { // Letterbox media. has similar added directly in // its element. diff --git a/src/scss/drawer-inject.scss b/src/scss/drawer-inject.scss new file mode 100644 index 00000000..30b060ac --- /dev/null +++ b/src/scss/drawer-inject.scss @@ -0,0 +1,34 @@ +: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); +} + +#fs { + width: 100%; + inset: 0; +} + +#ifs { + // Override width/height to be 100% instead of 100v[wh]. + height: 100%; +}; \ No newline at end of file diff --git a/src/scss/drawer.scss b/src/scss/drawer.scss new file mode 100644 index 00000000..3d23bb67 --- /dev/null +++ b/src/scss/drawer.scss @@ -0,0 +1,3 @@ +side-drawer { + background-color: var(--card-background-color); +} diff --git a/src/scss/timeline.scss b/src/scss/timeline.scss index de85c980..ff274ecc 100644 --- a/src/scss/timeline.scss +++ b/src/scss/timeline.scss @@ -6,6 +6,9 @@ display: block; background-color: var(--card-background-color); padding-bottom: 5px; + + // So that absolute sidedrawer is relative to this host. + position: relative; } div.timeline {