Merge pull request #767 from dermotduffy/refactor-carousels-composition
Complete carousel code refactor
This commit is contained in:
@@ -566,6 +566,11 @@ timeline:
|
||||
|
||||
### Dimensions Options
|
||||
|
||||
These options control the aspect-ratio of the entire card to make placement in
|
||||
Home Assistant dashboards more stable. Aspect ratio configuration applies once
|
||||
to the entire card (including the menu, thumbnails, etc), not just to displayed
|
||||
media.
|
||||
|
||||
All configuration is under:
|
||||
|
||||
```yaml
|
||||
@@ -2676,6 +2681,10 @@ See [screenshot above](#screenshots-card-casting).
|
||||
|
||||
You must be using a version of the [Frigate integration](https://github.com/blakeblackshear/frigate-hass-integration) >= 3.0.0-rc.2 to see recordings. Using an older version of the integration may also show blank thumbnails in the events viewer. Please upgrade your integration accordingly.
|
||||
|
||||
### Chrome autoplays when a tab becomes visible again
|
||||
|
||||
Even if `live.auto_play` or `media_viewer.auto_play` is set to `never`, Chrome itself will still auto play a video that was previously playing prior to the tab being hidden, once that tab is visible again. This behavior cannot be influenced by the card. Other browsers (e.g. Firefox, Safari) do not exhibit this behavior.
|
||||
|
||||
<a name="jsmpeg-troubleshooting"></a>
|
||||
|
||||
### JSMPEG Live Camera Only Shows A 'spinner'
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@
|
||||
"crypto": "^1.0.1",
|
||||
"custom-card-helpers": "^1.9.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"embla-carousel": "^6.2.0",
|
||||
"embla-carousel-wheel-gestures": "^2.1.1",
|
||||
"embla-carousel": "^7.0.0-rc05",
|
||||
"embla-carousel-wheel-gestures": "^3.0.0-rc01",
|
||||
"home-assistant-js-websocket": "^7.1.0",
|
||||
"keycharm": "^0.4.0",
|
||||
"lit": "^2.2.5",
|
||||
|
||||
+235
-51
@@ -1,39 +1,191 @@
|
||||
import EmblaCarousel, {
|
||||
EmblaCarouselType,
|
||||
EmblaOptionsType,
|
||||
EmblaPluginType
|
||||
} from 'embla-carousel';
|
||||
import { CSSResultGroup, LitElement, PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel';
|
||||
import { EmblaNodesType } from 'embla-carousel/components';
|
||||
import {
|
||||
CreatePluginType,
|
||||
EmblaPluginsType,
|
||||
LoosePluginType,
|
||||
} from 'embla-carousel/components/Plugins';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { throttle } from 'lodash-es';
|
||||
import carouselStyle from '../scss/carousel.scss';
|
||||
import { TransitionEffect } from '../types';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
|
||||
export interface CarouselSelect {
|
||||
index: number;
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
export type EmblaCarouselPlugins = CreatePluginType<
|
||||
LoosePluginType,
|
||||
Record<string, unknown>
|
||||
>[];
|
||||
|
||||
@customElement('frigate-card-carousel')
|
||||
export class FrigateCardCarousel extends LitElement {
|
||||
@property({ attribute: true, reflect: true })
|
||||
public direction: 'vertical' | 'horizontal' = 'horizontal';
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselOptions?: EmblaOptionsType;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
protected _refSlot: Ref<HTMLSlotElement> = createRef();
|
||||
|
||||
protected _carousel?: EmblaCarouselType;
|
||||
protected _plugins: Record<string, EmblaPluginType> = {};
|
||||
|
||||
// Whether the carousel is actively scrolling.
|
||||
protected _scrolling = false;
|
||||
|
||||
// Whether to reinit the carousel when it settles.
|
||||
protected _reInitOnSettle = false;
|
||||
|
||||
protected _carouselReInitInPlace = throttle(
|
||||
this._carouselReInitInPlaceInternal.bind(this),
|
||||
500,
|
||||
{ trailing: true },
|
||||
);
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
// Guarantee a re-render if the component is reconnected. See note in
|
||||
// disconnectedCallback().
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
// Destroy the carousel when the component is disconnected, which forces the
|
||||
// plugins (which may have registered event handlers) to also be destroyed.
|
||||
// The carousel will automatically reconstruct if the component is re-rendered.
|
||||
this._destroyCarousel();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the carousel if certain properties change.
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
const destroyProperties = [
|
||||
'direction',
|
||||
'carouselOptions',
|
||||
'carouselPlugins',
|
||||
] as const;
|
||||
if (destroyProperties.some((prop) => changedProps.has(prop))) {
|
||||
this._destroyCarousel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to a particular slide.
|
||||
* @param index Slide number.
|
||||
*/
|
||||
carouselScrollTo(index: number): void {
|
||||
this._carousel?.scrollTo(index, this._getTransitionEffect() === 'none');
|
||||
public carouselScrollTo(index: number): void {
|
||||
this._carousel?.scrollTo(index, this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previous slide.
|
||||
*/
|
||||
public carouselScrollPrevious(): void {
|
||||
this._carousel?.scrollPrev(this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the next slide.
|
||||
*/
|
||||
public carouselScrollNext(): void {
|
||||
this._carousel?.scrollNext(this.transitionEffect === 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected slide.
|
||||
* @returns The slide index or undefined if the carousel is not loaded.
|
||||
* @returns A CarouselSelect object (index & element).
|
||||
*/
|
||||
carouselSelected(): number | undefined {
|
||||
return this._carousel?.selectedScrollSnap();
|
||||
public getCarouselSelected(): CarouselSelect | null {
|
||||
const index = this._carousel?.selectedScrollSnap();
|
||||
const element =
|
||||
index !== undefined ? this._carousel?.slideNodes()[index] ?? null : null;
|
||||
if (index !== undefined && element) {
|
||||
return {
|
||||
index: index,
|
||||
element: element,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the carousel.
|
||||
*/
|
||||
public carouselClickAllowed(): boolean {
|
||||
return this._carousel?.clickAllowed() ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the carousel.
|
||||
*/
|
||||
public carousel(): EmblaCarouselType | null {
|
||||
return this._carousel ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReInit the carousel.
|
||||
*/
|
||||
protected _carouselReInit(options?: EmblaOptionsType): void {
|
||||
// Allow the browser a moment to paint components that are inflight, to
|
||||
// ensure accurate measurements are taken during the carousel
|
||||
// reinitialization.
|
||||
window.requestAnimationFrame(() => {
|
||||
this._carousel?.reInit({ ...options });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* ReInit the carousel but stay on the current slide.
|
||||
*/
|
||||
protected _carouselReInitInPlaceInternal(): void {
|
||||
const selected = this.getCarouselSelected();
|
||||
|
||||
this._carouselReInit({
|
||||
...(selected && { startIndex: selected.index }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ReInit the carousel when it is safe to do so without disturbing the
|
||||
* appearance (i.e. cutting off a scroll in progress).
|
||||
*/
|
||||
public carouselReInitWhenSafe(): void {
|
||||
if (this._scrolling) {
|
||||
this._reInitOnSettle = true;
|
||||
} else {
|
||||
this._carouselReInitInPlace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the live carousel plugins.
|
||||
*/
|
||||
public getCarouselPlugins(): EmblaPluginsType | null {
|
||||
return this._carousel?.plugins() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,35 +205,10 @@ export class FrigateCardCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
*/
|
||||
protected _getTransitionEffect(): TransitionEffect | undefined {
|
||||
return 'slide';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla options to use.
|
||||
* @returns An EmblaOptionsType object or undefined for no options.
|
||||
*/
|
||||
protected _getOptions(): EmblaOptionsType | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla plugins to use.
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected _destroyCarousel(): void {
|
||||
if (this._carousel) {
|
||||
this._carousel.destroy();
|
||||
}
|
||||
this._plugins = {};
|
||||
this._carousel = undefined;
|
||||
}
|
||||
|
||||
@@ -93,33 +220,84 @@ export class FrigateCardCarousel extends LitElement {
|
||||
'.embla__viewport',
|
||||
) as HTMLElement;
|
||||
|
||||
if (carouselNode) {
|
||||
const plugins = this._getPlugins() ?? [];
|
||||
this._plugins = plugins.reduce((acc, cur) => {
|
||||
acc[cur.name] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
const nodes: EmblaNodesType = {
|
||||
root: carouselNode,
|
||||
// As the slides are slotted, need to explicitly pull them out and pass
|
||||
// them to Embla.
|
||||
slides: this._refSlot.value?.assignedElements({ flatten: true }) as HTMLElement[],
|
||||
};
|
||||
|
||||
if (carouselNode && nodes.slides) {
|
||||
this._carousel = EmblaCarousel(
|
||||
carouselNode,
|
||||
nodes,
|
||||
{
|
||||
axis: this.direction == 'horizontal' ? 'x' : 'y',
|
||||
...this._getOptions(),
|
||||
speed: 20,
|
||||
...this.carouselOptions,
|
||||
},
|
||||
plugins,
|
||||
this.carouselPlugins,
|
||||
);
|
||||
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
|
||||
this._carousel.on('select', () => {
|
||||
const selected = this.carouselSelected();
|
||||
if (selected !== undefined) {
|
||||
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', {
|
||||
index: selected,
|
||||
const selected = this.getCarouselSelected();
|
||||
if (selected) {
|
||||
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:select', selected);
|
||||
}
|
||||
|
||||
// Make sure every select causes a refresh to allow for re-paint of the
|
||||
// next/previous controls.
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this._carousel.on('scroll', () => {
|
||||
this._scrolling = true;
|
||||
});
|
||||
this._carousel.on('settle', () => {
|
||||
// Reinitialize the carousel if a request to reinitialize was made
|
||||
// during scrolling (instead the request is handled after the scrolling
|
||||
// has settled).
|
||||
this._scrolling = false;
|
||||
if (this._reInitOnSettle) {
|
||||
this._reInitOnSettle = false;
|
||||
this._carouselReInitInPlace();
|
||||
}
|
||||
});
|
||||
this._carousel.on('settle', () => {
|
||||
const selected = this.getCarouselSelected();
|
||||
if (selected) {
|
||||
dispatchFrigateCardEvent<CarouselSelect>(this, 'carousel:settle', selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the slotted children in the carousel change.
|
||||
*/
|
||||
protected _slotChanged(): void {
|
||||
// Cannot just re-init, because the slide elements themselves may have
|
||||
// changed, and only a carousel init can pass in new (slotted) children.
|
||||
this._destroyCarousel();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const slides = this._refSlot.value?.assignedElements({ flatten: true }) || [];
|
||||
const currentSlide = this._carousel?.selectedScrollSnap() ?? 0;
|
||||
const showPrevious = this.carouselOptions?.loop || currentSlide > 0;
|
||||
const showNext = this.carouselOptions?.loop || currentSlide + 1 < slides.length;
|
||||
|
||||
return html` <div class="embla">
|
||||
${showPrevious ? html`<slot name="previous"></slot>` : ``}
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">
|
||||
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged.bind(this)}></slot>
|
||||
</div>
|
||||
</div>
|
||||
${showNext ? html`<slot name="next"></slot>` : ``}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
@@ -127,3 +305,9 @@ export class FrigateCardCarousel extends LitElement {
|
||||
return unsafeCSS(carouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frigate-card-carousel': FrigateCardCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export class FrigateCardDrawer extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged}></slot>
|
||||
<slot ${ref(this._refSlot)} @slotchange=${this._slotChanged.bind(this)}></slot>
|
||||
</side-drawer>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel';
|
||||
import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel';
|
||||
import { CreateOptionsType } from 'embla-carousel/components/Options.js';
|
||||
import { CreatePluginType } from 'embla-carousel/components/Plugins.js';
|
||||
import {
|
||||
AutoMuteCondition,
|
||||
AutoPauseCondition,
|
||||
@@ -7,8 +9,8 @@ import {
|
||||
FrigateCardMediaPlayer,
|
||||
} from '../../types.js';
|
||||
|
||||
export type AutoMediaPluginOptionsType = {
|
||||
playerSelector: string;
|
||||
type OptionsType = CreateOptionsType<{
|
||||
playerSelector?: string;
|
||||
|
||||
// Note: Neither play nor unmute will activate on selection. The caller is
|
||||
// expected to call the `play()` or `unmute()` methods manually when the media
|
||||
@@ -18,16 +20,30 @@ export type AutoMediaPluginOptionsType = {
|
||||
autoUnmuteCondition?: AutoUnmuteCondition;
|
||||
autoPauseCondition?: AutoPauseCondition;
|
||||
autoMuteCondition?: AutoMuteCondition;
|
||||
}>;
|
||||
|
||||
const defaultOptions: OptionsType = {
|
||||
active: true,
|
||||
breakpoints: {},
|
||||
};
|
||||
|
||||
export const defaultOptions: Partial<AutoMediaPluginOptionsType> = {};
|
||||
export type AutoMediaOptionsType = Partial<OptionsType>
|
||||
|
||||
export type AutoMediaPluginType = EmblaPluginType<AutoMediaPluginOptionsType> & {
|
||||
export type AutoMediaType = CreatePluginType<
|
||||
{
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
mute: () => void;
|
||||
unmute: () => void;
|
||||
};
|
||||
},
|
||||
AutoMediaOptionsType
|
||||
>;
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
autoMedia?: AutoMediaType
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Embla plugin to take automated actions on media (e.g. pause, unmute, etc).
|
||||
@@ -35,10 +51,15 @@ export type AutoMediaPluginType = EmblaPluginType<AutoMediaPluginOptionsType> &
|
||||
* @returns
|
||||
*/
|
||||
export function AutoMediaPlugin(
|
||||
userOptions?: AutoMediaPluginOptionsType,
|
||||
): AutoMediaPluginType {
|
||||
const options = Object.assign({}, defaultOptions, userOptions);
|
||||
userOptions?: AutoMediaOptionsType,
|
||||
): AutoMediaType {
|
||||
const optionsHandler = EmblaCarousel.optionsHandler();
|
||||
const optionsBase = optionsHandler.merge(
|
||||
defaultOptions,
|
||||
AutoMediaPlugin.globalOptions,
|
||||
);
|
||||
|
||||
let options: AutoMediaType['options'];
|
||||
let carousel: EmblaCarouselType;
|
||||
let slides: HTMLElement[];
|
||||
|
||||
@@ -47,6 +68,7 @@ export function AutoMediaPlugin(
|
||||
*/
|
||||
function init(embla: EmblaCarouselType): void {
|
||||
carousel = embla;
|
||||
options = optionsHandler.atMedia(self.options);
|
||||
slides = carousel.slideNodes();
|
||||
|
||||
// Frigate card media autoplays when the media loads not necessarily when the
|
||||
@@ -96,7 +118,7 @@ export function AutoMediaPlugin(
|
||||
* Handle document visibility changes.
|
||||
*/
|
||||
function visibilityHandler(): void {
|
||||
if (document.visibilityState == 'hidden') {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
if (
|
||||
options.autoPauseCondition &&
|
||||
['all', 'hidden'].includes(options.autoPauseCondition)
|
||||
@@ -109,7 +131,7 @@ export function AutoMediaPlugin(
|
||||
) {
|
||||
muteAll();
|
||||
}
|
||||
} else if (document.visibilityState == 'visible') {
|
||||
} else if (document.visibilityState === 'visible') {
|
||||
if (
|
||||
options.autoPlayCondition &&
|
||||
['all', 'visible'].includes(options.autoPlayCondition)
|
||||
@@ -131,7 +153,9 @@ export function AutoMediaPlugin(
|
||||
* @returns A FrigateCardMediaPlayer object or `null`.
|
||||
*/
|
||||
function getPlayer(slide: HTMLElement | undefined): FrigateCardMediaPlayer | null {
|
||||
return slide?.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null;
|
||||
return options.playerSelector
|
||||
? (slide?.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,9 +218,9 @@ export function AutoMediaPlugin(
|
||||
}
|
||||
}
|
||||
|
||||
const self: AutoMediaPluginType = {
|
||||
name: 'AutoMediaPlugin',
|
||||
options,
|
||||
const self: AutoMediaType = {
|
||||
name: 'autoMedia',
|
||||
options: optionsHandler.merge(optionsBase, userOptions),
|
||||
init,
|
||||
destroy,
|
||||
play,
|
||||
@@ -206,3 +230,5 @@ export function AutoMediaPlugin(
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
AutoMediaPlugin.globalOptions = <AutoMediaOptionsType | undefined>undefined;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { EmblaCarouselType, EmblaEventType, EmblaPluginType } from 'embla-carousel';
|
||||
import { CreateOptionsType } from 'embla-carousel/components/Options';
|
||||
import { CreatePluginType } from 'embla-carousel/components/Plugins';
|
||||
import EmblaCarousel, { EmblaCarouselType, EmblaEventType } from 'embla-carousel';
|
||||
import { LazyUnloadCondition } from '../../types';
|
||||
|
||||
export type LazyloadOptionsType = {
|
||||
export type OptionsType = CreateOptionsType<{
|
||||
// Number of slides to lazyload left/right of selected (0 == only selected
|
||||
// slide).
|
||||
lazyLoadCount?: number;
|
||||
@@ -9,18 +11,33 @@ export type LazyloadOptionsType = {
|
||||
|
||||
lazyLoadCallback?: (index: number, slide: HTMLElement) => void;
|
||||
lazyUnloadCallback?: (index: number, slide: HTMLElement) => void;
|
||||
};
|
||||
}>;
|
||||
|
||||
export const defaultOptions: Partial<LazyloadOptionsType> = {
|
||||
export const defaultOptions: OptionsType = {
|
||||
active: true,
|
||||
breakpoints: {},
|
||||
lazyLoadCount: 0,
|
||||
};
|
||||
|
||||
export type LazyloadType = EmblaPluginType<LazyloadOptionsType> & {
|
||||
hasLazyloaded: (index: number) => boolean;
|
||||
};
|
||||
export type LazyloadOptionsType = Partial<OptionsType>;
|
||||
|
||||
export type LazyloadType = CreatePluginType<
|
||||
{
|
||||
hasLazyloaded(index: number): boolean;
|
||||
},
|
||||
LazyloadOptionsType
|
||||
>;
|
||||
|
||||
declare module 'embla-carousel/components/Plugins' {
|
||||
interface EmblaPluginsType {
|
||||
lazyload?: LazyloadType;
|
||||
}
|
||||
}
|
||||
|
||||
export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
|
||||
const options = Object.assign({}, defaultOptions, userOptions);
|
||||
const optionsHandler = EmblaCarousel.optionsHandler();
|
||||
const optionsBase = optionsHandler.merge(defaultOptions, Lazyload.globalOptions);
|
||||
let options: LazyloadType['options'];
|
||||
|
||||
let carousel: EmblaCarouselType;
|
||||
let slides: HTMLElement[];
|
||||
@@ -34,6 +51,7 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
|
||||
*/
|
||||
function init(embla: EmblaCarouselType): void {
|
||||
carousel = embla;
|
||||
options = optionsHandler.atMedia(self.options);
|
||||
slides = carousel.slideNodes();
|
||||
|
||||
if (options.lazyLoadCallback) {
|
||||
@@ -137,11 +155,13 @@ export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
|
||||
}
|
||||
|
||||
const self: LazyloadType = {
|
||||
name: 'Lazyload',
|
||||
options,
|
||||
name: 'lazyload',
|
||||
options: optionsHandler.merge(optionsBase, userOptions),
|
||||
init,
|
||||
destroy,
|
||||
hasLazyloaded,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
Lazyload.globalOptions = <LazyloadOptionsType | undefined>undefined;
|
||||
|
||||
@@ -86,7 +86,6 @@ export class FrigateCardImage extends LitElement {
|
||||
* Ensure there is a cached value before an update.
|
||||
* @param _changedProps The changed properties
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('imageConfig')) {
|
||||
if (this._cachedValueController) {
|
||||
|
||||
+105
-131
@@ -1,7 +1,7 @@
|
||||
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||
import { Task } from '@lit-labs/task';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { EmblaOptionsType } from 'embla-carousel';
|
||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -13,14 +13,19 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
|
||||
import { dispatchFrigateCardErrorEvent, renderProgressIndicator } from '../components/message.js';
|
||||
import {
|
||||
dispatchFrigateCardErrorEvent,
|
||||
renderProgressIndicator,
|
||||
} from '../components/message.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import liveFrigateStyle from '../scss/live-frigate.scss';
|
||||
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
|
||||
import liveWebRTCStyle from '../scss/live-webrtc.scss';
|
||||
import liveStyle from '../scss/live.scss';
|
||||
import liveCarouselStyle from '../scss/live-carousel.scss';
|
||||
import {
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
@@ -45,15 +50,18 @@ import {
|
||||
dispatchMediaShowEvent,
|
||||
} from '../utils/media-info.js';
|
||||
import { View } from '../view.js';
|
||||
import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import { FrigateCardMediaCarousel } from './media-carousel.js';
|
||||
import {
|
||||
FrigateCardMediaCarousel,
|
||||
wrapMediaShowEventForCarousel,
|
||||
} from './media-carousel.js';
|
||||
import { dispatchErrorMessageEvent } from './message.js';
|
||||
import './next-prev-control.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import './title-control.js';
|
||||
import './surround-thumbnails';
|
||||
import '../patches/ha-camera-stream';
|
||||
import { EmblaCarouselPlugins } from './carousel.js';
|
||||
|
||||
// Number of seconds a signed URL is valid for.
|
||||
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
@@ -182,7 +190,7 @@ export class FrigateCardLive extends LitElement {
|
||||
}
|
||||
|
||||
@customElement('frigate-card-live-carousel')
|
||||
export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
export class FrigateCardLiveCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -206,62 +214,46 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
|
||||
// Index between camera name and slide number.
|
||||
protected _cameraToSlide: Record<string, number> = {};
|
||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||
|
||||
/**
|
||||
* 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('cameras') || changedProperties.has('liveConfig'))
|
||||
) {
|
||||
// All of these properties may fundamentally change the contents/size of
|
||||
// the DOM, and the carousel should be reset when they change.
|
||||
this._destroyCarousel();
|
||||
}
|
||||
|
||||
super.updated(changedProperties);
|
||||
|
||||
const frigateCardMediaCarousel = this._refMediaCarousel.value;
|
||||
const frigateCardCarousel = frigateCardMediaCarousel?.frigateCardCarousel();
|
||||
|
||||
if (changedProperties.has('view')) {
|
||||
const oldView = changedProperties.get('view') as View | undefined;
|
||||
if (
|
||||
this._carousel &&
|
||||
frigateCardCarousel &&
|
||||
oldView &&
|
||||
this.view?.camera &&
|
||||
this.view?.camera != oldView.camera
|
||||
) {
|
||||
const slide: number | undefined = this._cameraToSlide[this.view.camera];
|
||||
if (slide !== undefined && slide !== this.carouselSelected()) {
|
||||
this.carouselScrollTo(slide);
|
||||
if (slide !== undefined && slide !== frigateCardCarousel.getCarouselSelected()?.index) {
|
||||
frigateCardCarousel.carouselScrollTo(slide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('preloaded')) {
|
||||
const automedia = this._plugins['AutoMediaPlugin'] as
|
||||
| AutoMediaPluginType
|
||||
| undefined;
|
||||
if (automedia) {
|
||||
if (
|
||||
frigateCardMediaCarousel &&
|
||||
frigateCardCarousel &&
|
||||
changedProperties.has('preloaded')
|
||||
) {
|
||||
// If this has changed to preloaded (i.e. is now loaded but in the
|
||||
// background) take the appropriate play/pause/mute/unmute actions.
|
||||
if (this.preloaded) {
|
||||
if (
|
||||
this.liveConfig?.auto_pause &&
|
||||
['all', 'unselected'].includes(this.liveConfig.auto_pause)
|
||||
) {
|
||||
automedia.pause();
|
||||
}
|
||||
if (
|
||||
this.liveConfig?.auto_mute &&
|
||||
['all', 'unselected'].includes(this.liveConfig.auto_mute)
|
||||
) {
|
||||
automedia.mute();
|
||||
}
|
||||
frigateCardMediaCarousel.autoPause();
|
||||
frigateCardMediaCarousel.autoMute();
|
||||
} else {
|
||||
this._autoPlayHandler();
|
||||
this._autoUnmuteHandler();
|
||||
}
|
||||
frigateCardMediaCarousel.autoPlay();
|
||||
frigateCardMediaCarousel.autoUnmute();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,8 +262,11 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
*/
|
||||
protected _getTransitionEffect(): TransitionEffect | undefined {
|
||||
return this.liveConfig?.transition_effect;
|
||||
protected _getTransitionEffect(): TransitionEffect {
|
||||
return (
|
||||
this.liveConfig?.transition_effect ??
|
||||
frigateCardConfigDefaults.live.transition_effect
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,9 +288,8 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
* Get the Embla plugins to use.
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
protected _getPlugins(): EmblaCarouselPlugins {
|
||||
return [
|
||||
...super._getPlugins(),
|
||||
// Only enable wheel plugin if there is more than one camera.
|
||||
...(this.cameras && this.cameras.size > 1
|
||||
? [
|
||||
@@ -334,30 +328,6 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the loaded slide.
|
||||
*/
|
||||
protected _autoPlayHandler(): void {
|
||||
if (
|
||||
this.liveConfig?.auto_play &&
|
||||
['all', 'selected'].includes(this.liveConfig.auto_play)
|
||||
) {
|
||||
super._autoPlayHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the loaded slide.
|
||||
*/
|
||||
protected _autoUnmuteHandler(): void {
|
||||
if (
|
||||
this.liveConfig?.auto_unmute &&
|
||||
['all', 'selected'].includes(this.liveConfig.auto_unmute)
|
||||
) {
|
||||
super._autoUnmuteHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of slides to lazily load. 0 means all slides are lazy
|
||||
* loaded, 1 means that 1 slide on each side of the currently selected slide
|
||||
@@ -396,15 +366,18 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): void {
|
||||
if (!this._carousel || !this.view || !this.cameras) {
|
||||
protected _setViewHandler(): void {
|
||||
const selectedCameraIndex = this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.getCarouselSelected()
|
||||
?.index;
|
||||
if (selectedCameraIndex === undefined || !this.view || !this.cameras) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSnap = this._carousel.selectedScrollSnap();
|
||||
this.view
|
||||
.evolve({
|
||||
camera: Array.from(this.cameras.keys())[selectedSnap],
|
||||
camera: Array.from(this.cameras.keys())[selectedCameraIndex],
|
||||
|
||||
// Reset the target so thumbnails will be re-fetched.
|
||||
target: null,
|
||||
@@ -421,9 +394,13 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
protected _lazyloadOrUnloadSlide(
|
||||
action: 'load' | 'unload',
|
||||
_index: number,
|
||||
slide: HTMLElement,
|
||||
slide: Element,
|
||||
): void {
|
||||
const liveProvider = slide.querySelector(
|
||||
if (slide instanceof HTMLSlotElement) {
|
||||
slide = slide.assignedElements({ flatten: true })[0];
|
||||
}
|
||||
|
||||
const liveProvider = slide?.querySelector(
|
||||
'frigate-card-live-provider',
|
||||
) as FrigateCardLiveProvider;
|
||||
if (liveProvider) {
|
||||
@@ -453,18 +430,21 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
conditionState,
|
||||
) as LiveConfig;
|
||||
|
||||
return html` <div class="embla__slide">
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
<frigate-card-live-provider
|
||||
?disabled=${this.liveConfig.lazy_load}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.label=${getCameraTitle(this.hass, cameraConfig)}
|
||||
.liveConfig=${config}
|
||||
.hass=${this.hass}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||
this._mediaShowEventHandler(slideIndex, e)}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => {
|
||||
wrapMediaShowEventForCarousel(slideIndex, e);
|
||||
}}
|
||||
>
|
||||
</frigate-card-live-provider>
|
||||
</div>`;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _getCameraNeighbors(): [CameraConfig | null, CameraConfig | null] {
|
||||
@@ -489,30 +469,6 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
return [prev, next];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updating of the next/previous controls when the carousel is moved.
|
||||
*/
|
||||
protected _selectSlideNextPreviousHandler(): void {
|
||||
const updateNextPreviousControl = (
|
||||
control: FrigateCardNextPreviousControl,
|
||||
direction: 'previous' | 'next',
|
||||
): void => {
|
||||
const [prev, next] = this._getCameraNeighbors();
|
||||
const target = direction == 'previous' ? prev : next;
|
||||
|
||||
control.disabled = target == null;
|
||||
control.title = getCameraTitle(this.hass, target);
|
||||
control.icon = getCameraIcon(this.hass, target);
|
||||
};
|
||||
|
||||
if (this._previousControlRef.value) {
|
||||
updateNextPreviousControl(this._previousControlRef.value, 'previous');
|
||||
}
|
||||
if (this._nextControlRef.value) {
|
||||
updateNextPreviousControl(this._nextControlRef.value, 'next');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
@@ -533,47 +489,71 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
|
||||
const [prev, next] = this._getCameraNeighbors();
|
||||
const title = getCameraTitle(this.hass, this.cameras.get(this.view.camera));
|
||||
|
||||
// Notes on the below:
|
||||
// - guard() is used to avoid reseting the carousel unless the
|
||||
// options/plugins actually change.
|
||||
// - the 'carousel:settle' event is listened for (instead of
|
||||
// 'carousel:select') to only trigger the view change (which subsequently
|
||||
// fetches thumbnails) after the carousel has stopped moving. This gives a
|
||||
// much smoother carousel experience since network fetches are not at the
|
||||
// same time as carousel movement (at a cost of fetching thumbnails a
|
||||
// little later).
|
||||
|
||||
return html`
|
||||
<div class="embla">
|
||||
<frigate-card-media-carousel
|
||||
${ref(this._refMediaCarousel)}
|
||||
.carouselOptions=${guard(
|
||||
[this.cameras, this.liveConfig],
|
||||
this._getOptions.bind(this),
|
||||
)}
|
||||
.carouselPlugins=${guard(
|
||||
[this.cameras, this.liveConfig],
|
||||
this._getPlugins.bind(this),
|
||||
) as EmblaCarouselPlugins}
|
||||
.label="${title ? `${localize('common.live')}: ${title}` : ''}"
|
||||
.titlePopupConfig=${config.controls.title}
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:carousel:settle=${this._setViewHandler.bind(this)}
|
||||
>
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._previousControlRef)}
|
||||
slot="previous"
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${config.controls.next_previous}
|
||||
.label=${getCameraTitle(this.hass, prev)}
|
||||
.icon=${getCameraIcon(this.hass, prev)}
|
||||
?disabled=${prev == null}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('previous');
|
||||
this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.carouselScrollPrevious();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</frigate-card-next-previous-control>
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</div>
|
||||
</div>
|
||||
${slides}
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._nextControlRef)}
|
||||
slot="next"
|
||||
.direction=${'next'}
|
||||
.controlConfig=${config.controls.next_previous}
|
||||
.label=${getCameraTitle(this.hass, next)}
|
||||
.icon=${getCameraIcon(this.hass, next)}
|
||||
?disabled=${next == null}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('next');
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</frigate-card-next-previous-control>
|
||||
</div>
|
||||
<frigate-card-title-control
|
||||
${ref(this._titleControlRef)}
|
||||
.config=${config.controls.title}
|
||||
.text="${title ? `${localize('common.live')}: ${title}` : ''}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control>
|
||||
</frigate-card-media-carousel>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('frigate-card-live-provider')
|
||||
@@ -749,20 +729,16 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
}
|
||||
|
||||
if (!this.cameraConfig?.camera_entity) {
|
||||
return dispatchErrorMessageEvent(
|
||||
this,
|
||||
localize('error.no_live_camera'),
|
||||
{ context: this.cameraConfig },
|
||||
);
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_live_camera'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
||||
if (!stateObj || stateObj.state === 'unavailable') {
|
||||
return dispatchErrorMessageEvent(
|
||||
this,
|
||||
localize('error.live_camera_unavailable'),
|
||||
{ context: this.cameraConfig },
|
||||
);
|
||||
return dispatchErrorMessageEvent(this, localize('error.live_camera_unavailable'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
@@ -1145,11 +1121,9 @@ export class FrigateCardLiveJSMPEG extends LitElement {
|
||||
this._jsmpegCanvasElement.className = 'media';
|
||||
|
||||
if (!this.cameraConfig?.frigate.camera_name) {
|
||||
return dispatchErrorMessageEvent(
|
||||
this,
|
||||
localize('error.no_camera_name'),
|
||||
{ context: this.cameraConfig },
|
||||
);
|
||||
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), {
|
||||
context: this.cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const url = await this._getURL();
|
||||
|
||||
+239
-141
@@ -1,16 +1,25 @@
|
||||
import { EmblaCarouselType } from 'embla-carousel';
|
||||
import { CSSResultGroup, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { createRef, Ref } from 'lit/directives/ref.js';
|
||||
import { EmblaOptionsType } from 'embla-carousel';
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import mediaCarouselStyle from '../scss/media-carousel.scss';
|
||||
import type { MediaShowInfo } from '../types.js';
|
||||
import type {
|
||||
MediaShowInfo,
|
||||
NextPreviousControlConfig,
|
||||
TitleControlConfig,
|
||||
TransitionEffect,
|
||||
} from '../types.js';
|
||||
import { dispatchFrigateCardEvent } from '../utils/basic';
|
||||
import {
|
||||
createMediaShowInfo,
|
||||
dispatchExistingMediaShowInfoAsEvent,
|
||||
isValidMediaShowInfo
|
||||
isValidMediaShowInfo,
|
||||
} from '../utils/media-info.js';
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import { AutoMediaPluginType } from './embla-plugins/automedia.js';
|
||||
import { CarouselSelect, EmblaCarouselPlugins, FrigateCardCarousel } from './carousel';
|
||||
import { AutoMediaType } from './embla-plugins/automedia.js';
|
||||
import './next-prev-control.js';
|
||||
import './carousel.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import { FrigateCardTitleControl } from './title-control.js';
|
||||
|
||||
@@ -18,8 +27,78 @@ const getEmptyImageSrc = (width: number, height: number) =>
|
||||
`data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
|
||||
export const IMG_EMPTY = getEmptyImageSrc(16, 9);
|
||||
|
||||
export interface CarouselMediaShowInfo {
|
||||
slide: number;
|
||||
mediaShowInfo: MediaShowInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a carousel media show event.
|
||||
* @param target The target to send it from.
|
||||
* @param carouselMediaShowInfo The CarouselMediaShowInfo.
|
||||
*/
|
||||
const dispatchFrigateCardCarouselMediaShow = (
|
||||
target: EventTarget,
|
||||
carouselMediaShowInfo: CarouselMediaShowInfo,
|
||||
): void => {
|
||||
dispatchFrigateCardEvent<CarouselMediaShowInfo>(
|
||||
target,
|
||||
'carousel:media-show',
|
||||
carouselMediaShowInfo,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn a MediaShowEvent into a CarouselMediaShowInfo.
|
||||
* @param slide The slide number.
|
||||
* @param event The MediaShowEvent.
|
||||
*/
|
||||
export const wrapMediaShowEventForCarousel = (
|
||||
slide: number,
|
||||
event: CustomEvent<MediaShowInfo>,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], {
|
||||
slide: slide,
|
||||
mediaShowInfo: event.detail,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn a (stock) media load event into a CarouselMediaShowInfo.
|
||||
* @param slide The slide number.
|
||||
* @param event The MediaShowEvent.
|
||||
*/
|
||||
export const wrapMediaLoadEventForCarousel = (slide: number, event: Event) => {
|
||||
const mediaShowInfo = createMediaShowInfo(event);
|
||||
if (mediaShowInfo) {
|
||||
dispatchFrigateCardCarouselMediaShow(event.composedPath()[0], {
|
||||
slide: slide,
|
||||
mediaShowInfo: mediaShowInfo,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@customElement('frigate-card-media-carousel')
|
||||
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
export class FrigateCardMediaCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public nextPreviousConfig?: NextPreviousControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselOptions?: EmblaOptionsType;
|
||||
|
||||
@property({ attribute: false })
|
||||
public carouselPlugins?: EmblaCarouselPlugins;
|
||||
|
||||
@property({ attribute: true })
|
||||
public transitionEffect?: TransitionEffect;
|
||||
|
||||
@property({ attribute: false })
|
||||
public label?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public titlePopupConfig?: TitleControlConfig;
|
||||
|
||||
// A "map" from slide number to MediaShowInfo object.
|
||||
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
|
||||
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
|
||||
@@ -27,34 +106,100 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
protected _titleControlRef: Ref<FrigateCardTitleControl> = createRef();
|
||||
protected _titleTimerID: number | null = null;
|
||||
|
||||
protected _boundAutoPlayHandler = this.autoPlay.bind(this);
|
||||
protected _boundAutoUnmuteHandler = this.autoUnmute.bind(this);
|
||||
protected _boundAdaptContainerHeightToSlide =
|
||||
this._adaptContainerHeightToSlide.bind(this);
|
||||
protected _boundTitleHandler = this._titleHandler.bind(this);
|
||||
|
||||
// This carousel may be resized by Lovelace resizes, window resizes,
|
||||
// fullscreen, etc. Always call the adaptive height handler when the size
|
||||
// changes.
|
||||
protected _resizeObserver: ResizeObserver;
|
||||
protected _slideResizeObserver: ResizeObserver;
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
|
||||
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._resizeObserver = new ResizeObserver(this._adaptiveHeightHandler.bind(this));
|
||||
// Need to watch both changes in this element (e.g. caused by a window
|
||||
// resize or fullscreen change) and changes in the selected slide itself
|
||||
// (e.g. changing from a progress indicator to a loaded media).
|
||||
this._resizeObserver = new ResizeObserver(this._reInitAndAdjustHeight.bind(this));
|
||||
this._slideResizeObserver = new ResizeObserver(
|
||||
this._reInitAndAdjustHeight.bind(this),
|
||||
);
|
||||
this._intersectionObserver = new IntersectionObserver(
|
||||
this._intersectionHandler.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the selected slide. May be overridden to control when
|
||||
* autoplay should happen.
|
||||
* Get the underlying carousel.
|
||||
*/
|
||||
protected _autoPlayHandler(): void {
|
||||
(this._plugins['AutoMediaPlugin'] as AutoMediaPluginType | undefined)?.play();
|
||||
public frigateCardCarousel(): FrigateCardCarousel | null {
|
||||
return this._refCarousel.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the selected slide. May be overridden to control when
|
||||
* autoplay should happen.
|
||||
* Get the AutoMedia plugin (if any).
|
||||
* @returns The plugin or `null`.
|
||||
*/
|
||||
protected _autoUnmuteHandler(): void {
|
||||
(this._plugins['AutoMediaPlugin'] as AutoMediaPluginType | undefined)?.unmute();
|
||||
protected _getAutoMediaPlugin(): AutoMediaType | null {
|
||||
return this.frigateCardCarousel()?.carousel()?.plugins().autoMedia ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the selected slide.
|
||||
*/
|
||||
public autoPlay(): void {
|
||||
const automediaOptions = this._getAutoMediaPlugin()?.options;
|
||||
if (
|
||||
automediaOptions?.autoPlayCondition &&
|
||||
['all', 'selected'].includes(automediaOptions?.autoPlayCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the media on the selected slide.
|
||||
*/
|
||||
public autoPause(): void {
|
||||
const automediaOptions = this._getAutoMediaPlugin()?.options;
|
||||
if (
|
||||
automediaOptions?.autoPauseCondition &&
|
||||
['all', 'selected'].includes(automediaOptions.autoPauseCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the selected slide.
|
||||
*/
|
||||
public autoUnmute(): void {
|
||||
const automediaOptions = this._getAutoMediaPlugin()?.options;
|
||||
if (
|
||||
automediaOptions?.autoUnmuteCondition &&
|
||||
['all', 'selected'].includes(automediaOptions?.autoUnmuteCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.unmute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute the media on the selected slide.
|
||||
*/
|
||||
public autoMute(): void {
|
||||
const automediaOptions = this._getAutoMediaPlugin()?.options;
|
||||
if (
|
||||
automediaOptions?.autoMuteCondition &&
|
||||
['all', 'selected'].includes(automediaOptions?.autoMuteCondition)
|
||||
) {
|
||||
this._getAutoMediaPlugin()?.mute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,8 +223,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
|
||||
// Allow a brief pause after the media loads, but before the title is
|
||||
// displayed. This allows for a pleasant appearance/disappear of the title,
|
||||
// and allows for the browser to finish rendering the carousel (inc.
|
||||
// adaptive height which has `0.5s ease`, see `media-carousel.scss`).
|
||||
// and allows for the browser to finish rendering the carousel.
|
||||
this._titleTimerID = window.setTimeout(show, 0.5 * 1000);
|
||||
}
|
||||
|
||||
@@ -88,10 +232,14 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('frigate-card:media-show', this._autoPlayHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._autoUnmuteHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._adaptiveHeightHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._titleHandler);
|
||||
|
||||
this.addEventListener('frigate-card:media-show', this._boundAutoPlayHandler);
|
||||
this.addEventListener('frigate-card:media-show', this._boundAutoUnmuteHandler);
|
||||
this.addEventListener(
|
||||
'frigate-card:media-show',
|
||||
this._boundAdaptContainerHeightToSlide,
|
||||
);
|
||||
this.addEventListener('frigate-card:media-show', this._boundTitleHandler);
|
||||
this._resizeObserver.observe(this);
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
@@ -100,13 +248,25 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener('frigate-card:media-show', this._autoPlayHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._autoUnmuteHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._adaptiveHeightHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._titleHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._boundAutoPlayHandler);
|
||||
this.removeEventListener('frigate-card:media-show', this._boundAutoUnmuteHandler);
|
||||
this.removeEventListener(
|
||||
'frigate-card:media-show',
|
||||
this._boundAdaptContainerHeightToSlide,
|
||||
);
|
||||
this.removeEventListener('frigate-card:media-show', this._boundTitleHandler);
|
||||
this._resizeObserver.disconnect();
|
||||
this._intersectionObserver.disconnect();
|
||||
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* ReInit the carousel and adapt the container height.
|
||||
*/
|
||||
protected _reInitAndAdjustHeight(): void {
|
||||
this.frigateCardCarousel()?.carouselReInitWhenSafe();
|
||||
this._adaptContainerHeightToSlide();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,76 +284,28 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
* - Example bug when this reinitialization is not performed:
|
||||
* https://github.com/dermotduffy/frigate-hass-card/issues/651
|
||||
*/
|
||||
|
||||
const reInit = (): void => {
|
||||
// Safari appears to not loop the carousel unless the options are passed
|
||||
// back in during re-initialization.
|
||||
this._carousel?.reInit(this._getOptions());
|
||||
};
|
||||
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
// For performance, run the reinit in idle cycles if the browser supports
|
||||
// it, but only give it 400ms before running as it may otherwise be
|
||||
// noticeable to the user.
|
||||
if (window.requestIdleCallback !== undefined) {
|
||||
window.requestIdleCallback(reInit, { timeout: 400 });
|
||||
} else {
|
||||
reInit();
|
||||
this._reInitAndAdjustHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected _destroyCarousel(): void {
|
||||
super._destroyCarousel();
|
||||
|
||||
// Notes on instance variables:
|
||||
// * this._mediaShowInfo: This is set when the media in the DOM loads. If a
|
||||
// new View included the same media, the DOM would not change and so the
|
||||
// prior contents would still be valid and would not re-appear (as the
|
||||
// media would not reload) -- as such, leave this alone on carousel
|
||||
// destroy. New media in that slide will replace the prior contents on
|
||||
// load.
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the carousel.
|
||||
*/
|
||||
protected _initCarousel(): void {
|
||||
super._initCarousel();
|
||||
|
||||
// Necessary because typescript local type narrowing is not paying attention
|
||||
// to the side-effect of the call to super._initCarousel().
|
||||
const carousel = this._carousel as EmblaCarouselType | undefined;
|
||||
|
||||
// Update the view object as the carousel is moved.
|
||||
carousel?.on('select', this._selectSlideSetViewHandler.bind(this));
|
||||
|
||||
// Update the next/previous controls as the carousel is moved.
|
||||
carousel?.on('select', this._selectSlideNextPreviousHandler.bind(this));
|
||||
|
||||
// Dispatch MediaShow events as the carousel is moved.
|
||||
carousel?.on('init', this._selectSlideMediaShowHandler.bind(this));
|
||||
carousel?.on('select', this._selectSlideMediaShowHandler.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the the height of the container on media load in case the dimensions
|
||||
* Set the the height of the component on media load in case the dimensions
|
||||
* have changed. This handler is not triggered from carousel events, as it's
|
||||
* actually the media load/show that will change the dimensions, and that is
|
||||
* async from carousel actions (e.g. lazy-loaded media).
|
||||
*
|
||||
* This component does not use the stock Embla auto-height plugin as it
|
||||
* resizes the container on selection rather than media load.
|
||||
*/
|
||||
protected _adaptiveHeightHandler(): void {
|
||||
protected _adaptContainerHeightToSlide(): void {
|
||||
const adaptCarouselHeight = (): void => {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
const slide = this._carousel?.selectedScrollSnap();
|
||||
if (slide !== undefined) {
|
||||
this._carousel.containerNode().style.removeProperty('max-height');
|
||||
const slides = this._carousel.slideNodes();
|
||||
const height = slides[slide].getBoundingClientRect().height;
|
||||
if (height > 0) {
|
||||
this._carousel.containerNode().style.maxHeight = `${height}px`;
|
||||
const selected = this.frigateCardCarousel()?.getCarouselSelected();
|
||||
if (selected) {
|
||||
this.style.removeProperty('max-height');
|
||||
const height = selected.element.getBoundingClientRect().height;
|
||||
if (height !== undefined && height > 0) {
|
||||
this.style.maxHeight = `${height}px`;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -208,42 +320,12 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
window.requestAnimationFrame(adaptCarouselHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): void {
|
||||
// To be overridden in children.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updating of the next/previous controls when the carousel is moved.
|
||||
*/
|
||||
protected _selectSlideNextPreviousHandler(): void {
|
||||
// To be overridden in children.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a next/previous control interaction.
|
||||
* @param direction The direction requested, previous or next.
|
||||
*/
|
||||
protected _nextPreviousHandler(direction: 'previous' | 'next'): void {
|
||||
if (direction === 'previous') {
|
||||
this._carousel?.scrollPrev(this._getTransitionEffect() === 'none');
|
||||
} else if (direction === 'next') {
|
||||
this._carousel?.scrollNext(this._getTransitionEffect() === 'none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _selectSlideMediaShowHandler(): void {
|
||||
if (!this._carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slideIndex = this._carousel.selectedScrollSnap();
|
||||
if (slideIndex in this._mediaShowInfo) {
|
||||
protected _dispatchMediaShowInfo(): void {
|
||||
const slideIndex = this.frigateCardCarousel()?.getCarouselSelected()?.index;
|
||||
if (slideIndex !== undefined && slideIndex in this._mediaShowInfo) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]);
|
||||
}
|
||||
}
|
||||
@@ -254,46 +336,62 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
|
||||
* @param slideIndex The relevant slide index.
|
||||
* @param event The media-show event from the child component.
|
||||
*/
|
||||
protected _mediaShowEventHandler(
|
||||
slideIndex: number,
|
||||
event: CustomEvent<MediaShowInfo>,
|
||||
): void {
|
||||
protected _storeMediaShowInfo(event: CustomEvent<CarouselMediaShowInfo>): void {
|
||||
// Don't allow the inbound event to propagate upwards, that will be
|
||||
// automatically done at the appropriate time as the slide is shown.
|
||||
event.stopPropagation();
|
||||
this._mediaLoadedHandler(slideIndex, event.detail);
|
||||
}
|
||||
const mediaShowInfo = event.detail.mediaShowInfo;
|
||||
const slideIndex = event.detail.slide;
|
||||
|
||||
/**
|
||||
* Handle a MediaShowInfo object that is generated on media load, by saving it
|
||||
* for future, or immediate use, when the relevant slide is displayed.
|
||||
* @param slideIndex The relevant slide index.
|
||||
* @param mediaShowInfo The MediaShowInfo object generated by the media.
|
||||
*/
|
||||
protected _mediaLoadedHandler(
|
||||
slideIndex: number,
|
||||
mediaShowInfo?: MediaShowInfo | null,
|
||||
): void {
|
||||
// isValidMediaShowInfo is used to prevent saving media info that will be
|
||||
// rejected upstream (empty 1x1 images will be rejected here).
|
||||
if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) {
|
||||
this._mediaShowInfo[slideIndex] = mediaShowInfo;
|
||||
if (this._carousel && this._carousel?.selectedScrollSnap() === slideIndex) {
|
||||
if (this.frigateCardCarousel()?.getCarouselSelected()?.index === slideIndex) {
|
||||
dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
.carouselOptions=${this.carouselOptions}
|
||||
.carouselPlugins=${this.carouselPlugins}
|
||||
transitionEffect=${ifDefined(this.transitionEffect)}
|
||||
@frigate-card:carousel:init=${this._dispatchMediaShowInfo.bind(this)}
|
||||
@frigate-card:carousel:select=${(ev: CustomEvent<CarouselSelect>) => {
|
||||
this._slideResizeObserver.disconnect();
|
||||
this._slideResizeObserver.observe(ev.detail.element);
|
||||
this._dispatchMediaShowInfo();
|
||||
}}
|
||||
@frigate-card:carousel:media-show=${this._storeMediaShowInfo.bind(this)}
|
||||
>
|
||||
<slot slot="previous" name="previous"></slot>
|
||||
<slot></slot>
|
||||
<slot slot="next" name="next"></slot>
|
||||
</frigate-card-carousel>
|
||||
${this.label && this.titlePopupConfig
|
||||
? html`<frigate-card-title-control
|
||||
${ref(this._titleControlRef)}
|
||||
.config=${this.titlePopupConfig}
|
||||
.text="${this.label}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control> `
|
||||
: ``}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return [super.styles, unsafeCSS(mediaCarouselStyle)];
|
||||
return unsafeCSS(mediaCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"frigate-card-media-carousel": FrigateCardMediaCarousel
|
||||
'frigate-card-media-carousel': FrigateCardMediaCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public view?: Readonly<View>;
|
||||
|
||||
@property({ attribute: false })
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public config?: ThumbnailsControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -58,7 +58,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
*/
|
||||
protected async _fetchMedia(): Promise<void> {
|
||||
if (
|
||||
!fetch ||
|
||||
!this.fetch ||
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.config ||
|
||||
@@ -148,7 +148,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
.selected=${this.view.childIndex}
|
||||
.cameras=${this.cameras}
|
||||
@frigate-card:change-view=${(ev: CustomEvent) => changeDrawer(ev, 'close')}
|
||||
@frigate-card:carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||
@frigate-card:thumbnail-carousel:tap=${(ev: CustomEvent<ThumbnailCarouselTap>) => {
|
||||
// Send the view change from the source of the tap event, so the
|
||||
// view change will be caught by the handler above (to close the drawer).
|
||||
this.view
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||
import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||
import type { CameraConfig, FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateBrowseMediaSource,
|
||||
ThumbnailsControlConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||
import { isTrueMedia } from '../utils/ha/browse-media';
|
||||
import { View } from '../view.js';
|
||||
import { FrigateCardCarousel } from './carousel.js';
|
||||
import './thumbnail.js';
|
||||
import './carousel.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
export interface ThumbnailCarouselTap {
|
||||
slideIndex: number;
|
||||
@@ -20,7 +34,7 @@ export interface ThumbnailCarouselTap {
|
||||
}
|
||||
|
||||
@customElement('frigate-card-thumbnail-carousel')
|
||||
export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
export class FrigateCardThumbnailCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@@ -35,11 +49,28 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
@property({ attribute: false })
|
||||
public cameras?: Map<string, CameraConfig>;
|
||||
|
||||
protected _refCarousel: Ref<FrigateCardCarousel> = createRef();
|
||||
|
||||
// Thumbnail carousels can expand (e.g. drawer-based carousels after the main
|
||||
// media loads). The carousel must be re-initialized in these cases, or the
|
||||
// dynamic sizing fails (and users can scroll past the end of the carousel).
|
||||
protected _resizeObserver: ResizeObserver;
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: ThumbnailsControlConfig;
|
||||
|
||||
@state()
|
||||
protected _selected: number | null = null;
|
||||
|
||||
protected _carouselOptions?: EmblaOptionsType;
|
||||
protected _carouselPlugins: EmblaPluginType[] = [
|
||||
WheelGesturesPlugin({
|
||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||
// gestures as scrolling for the carousel.
|
||||
forceWheelAxis: 'y',
|
||||
}),
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||
@@ -48,36 +79,17 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
@property({ attribute: false })
|
||||
set selected(selected: number | null) {
|
||||
this._selected = selected;
|
||||
if (selected !== null) {
|
||||
// If there is a selection, 'dim' all the other slides.
|
||||
this.style.setProperty('--frigate-card-carousel-thumbnail-opacity', '0.4');
|
||||
this.style.setProperty(
|
||||
'--frigate-card-carousel-thumbnail-opacity',
|
||||
selected === null ? '1.0' : '0.4',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
set config(config: ThumbnailsControlConfig) {
|
||||
this.direction = ['left', 'right'].includes(config.mode) ? 'vertical' : 'horizontal';
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@state()
|
||||
protected _config?: ThumbnailsControlConfig;
|
||||
|
||||
@state()
|
||||
protected _selected?: number | null;
|
||||
|
||||
/**
|
||||
* Handle gallery resize.
|
||||
*/
|
||||
protected _resizeHandler(): void {
|
||||
if (this._carousel) {
|
||||
this._carousel.reInit();
|
||||
// Reinit will cause the scroll position to reset, so re-scroll to the
|
||||
// correct location.
|
||||
if (this._selected !== undefined && this._selected !== null) {
|
||||
this.carouselScrollTo(this._selected);
|
||||
}
|
||||
}
|
||||
this._refCarousel.value?.carouselReInitWhenSafe();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,23 +119,6 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
startIndex: this._selected ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Embla plugins to use.
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [
|
||||
...super._getPlugins(),
|
||||
// Only enable wheel plugin if there is more than one camera.
|
||||
WheelGesturesPlugin({
|
||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||
// gestures as scrolling for the carousel.
|
||||
forceWheelAxis: 'y',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slides to include in the render.
|
||||
* @returns The slides to include in the render.
|
||||
@@ -148,13 +143,24 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('_config')) {
|
||||
if (this._config?.size) {
|
||||
this.style.setProperty(
|
||||
'--frigate-card-thumbnail-size',
|
||||
`${this._config.size}px`,
|
||||
);
|
||||
if (changedProps.has('config')) {
|
||||
if (this.config?.size) {
|
||||
this.style.setProperty('--frigate-card-thumbnail-size', `${this.config.size}px`);
|
||||
}
|
||||
const direction = this._getDirection();
|
||||
if (direction) {
|
||||
this.setAttribute('direction', direction);
|
||||
} else {
|
||||
this.removeAttribute('direction');
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._carouselOptions) {
|
||||
// Want to set the initial carousel options just before the first render
|
||||
// in order to get the startIndex correct in the options. It is not safe
|
||||
// to rely on carouselScrollTo() post update, since the nested carousel
|
||||
// may not yet be actual rendered/created.
|
||||
this._carouselOptions = this._getOptions();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,17 +169,12 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
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);
|
||||
}
|
||||
if (this._selected !== null) {
|
||||
this._refCarousel.value?.carouselScrollTo(this._selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -209,17 +210,21 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
.target=${parent}
|
||||
.childIndex=${childIndex}
|
||||
.clientID=${cameraConfig?.frigate.client_id}
|
||||
?details=${this._config?.show_details}
|
||||
?show_favorite_control=${this._config?.show_favorite_control}
|
||||
?show_timeline_control=${this._config?.show_timeline_control}
|
||||
?details=${this.config?.show_details}
|
||||
?show_favorite_control=${this.config?.show_favorite_control}
|
||||
?show_timeline_control=${this.config?.show_timeline_control}
|
||||
class="${classMap(classes)}"
|
||||
@click=${(ev) => {
|
||||
if (this._carousel && this._carousel.clickAllowed()) {
|
||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(this, 'carousel:tap', {
|
||||
if (this._refCarousel.value?.carouselClickAllowed()) {
|
||||
dispatchFrigateCardEvent<ThumbnailCarouselTap>(
|
||||
this,
|
||||
'thumbnail-carousel:tap',
|
||||
{
|
||||
slideIndex: slideIndex,
|
||||
target: parent,
|
||||
childIndex: childIndex,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
@@ -227,33 +232,49 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel {
|
||||
</frigate-card-thumbnail>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direction of the thumbnail carousel.
|
||||
* @returns `vertical`, `horizontal` or undefined.
|
||||
*/
|
||||
protected _getDirection(): 'horizontal' | 'vertical' | undefined {
|
||||
if (this.config?.mode === 'left' || this.config?.mode === 'right') {
|
||||
return 'vertical';
|
||||
} else if (this.config?.mode === 'above' || this.config?.mode === 'below') {
|
||||
return 'horizontal';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const slides = this._getSlides();
|
||||
if (!slides.length || !this._config || this._config.mode == 'none') {
|
||||
if (!slides.length || !this.config || this.config.mode === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
return html` <div class="embla">
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
return html`<frigate-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
direction=${ifDefined(this._getDirection())}
|
||||
.carouselOptions=${this._carouselOptions}
|
||||
.carouselPlugins=${this._carouselPlugins}
|
||||
>
|
||||
${slides}
|
||||
</frigate-card-carousel>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return [super.styles, unsafeCSS(thumbnailCarouselStyle)];
|
||||
return unsafeCSS(thumbnailCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"frigate-card-thumbnail-carousel": FrigateCardThumbnailCarousel
|
||||
'frigate-card-thumbnail-carousel': FrigateCardThumbnailCarousel;
|
||||
}
|
||||
}
|
||||
|
||||
+100
-130
@@ -7,26 +7,29 @@ import {
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import {
|
||||
dispatchFrigateCardErrorEvent,
|
||||
renderProgressIndicator
|
||||
renderProgressIndicator,
|
||||
} from '../components/message.js';
|
||||
import viewerStyle from '../scss/viewer.scss';
|
||||
import type {
|
||||
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
||||
import {
|
||||
BrowseMediaNeighbors,
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
ExtendedHomeAssistant,
|
||||
FrigateBrowseMediaSource,
|
||||
frigateCardConfigDefaults,
|
||||
FrigateCardMediaPlayer,
|
||||
MediaShowInfo,
|
||||
TransitionEffect,
|
||||
ViewerConfig
|
||||
ViewerConfig,
|
||||
} from '../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
@@ -36,19 +39,23 @@ import {
|
||||
getFullDependentBrowseMediaQueryParametersOrDispatchError,
|
||||
isTrueMedia,
|
||||
multipleBrowseMediaQueryMerged,
|
||||
overrideMultiBrowseMediaQueryParameters
|
||||
overrideMultiBrowseMediaQueryParameters,
|
||||
} from '../utils/ha/browse-media.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||
import { createMediaShowInfo } from '../utils/media-info.js';
|
||||
import { View } from '../view.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
|
||||
import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js';
|
||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||
import {
|
||||
FrigateCardMediaCarousel,
|
||||
IMG_EMPTY,
|
||||
wrapMediaLoadEventForCarousel,
|
||||
wrapMediaShowEventForCarousel,
|
||||
} from './media-carousel.js';
|
||||
import './next-prev-control.js';
|
||||
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
|
||||
import './title-control.js';
|
||||
import "../patches/ha-hls-player";
|
||||
import "./surround-thumbnails";
|
||||
import '../patches/ha-hls-player';
|
||||
import './surround-thumbnails';
|
||||
import { EmblaCarouselPlugins } from './carousel.js';
|
||||
|
||||
@customElement('frigate-card-viewer')
|
||||
export class FrigateCardViewer extends LitElement {
|
||||
@@ -133,7 +140,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
const FRIGATE_CARD_HLS_SELECTOR = 'frigate-card-ha-hls-player';
|
||||
|
||||
@customElement('frigate-card-viewer-carousel')
|
||||
export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
export class FrigateCardViewerCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -154,6 +161,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
protected _refMediaCarousel: Ref<FrigateCardMediaCarousel> = createRef();
|
||||
|
||||
// Mapping of slide # to FrigateBrowseMediaSource child #.
|
||||
// (Folders are not media items that can be rendered).
|
||||
protected _slideToChild: Record<number, number> = {};
|
||||
@@ -187,22 +196,23 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* @param changedProperties The properties that were changed in this render.
|
||||
*/
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
if (this._carousel && changedProperties.has('viewerConfig')) {
|
||||
this._destroyCarousel();
|
||||
}
|
||||
const frigateCardCarousel = this._refMediaCarousel.value?.frigateCardCarousel();
|
||||
|
||||
if (this._carousel && changedProperties.has('view')) {
|
||||
if (frigateCardCarousel && changedProperties.has('view')) {
|
||||
const oldView = changedProperties.get('view') as View | undefined;
|
||||
if (oldView) {
|
||||
if (oldView.target !== this.view?.target) {
|
||||
// If the media target is different entirely, reset the carousel.
|
||||
this._destroyCarousel();
|
||||
} else if (this.view.childIndex != oldView.childIndex) {
|
||||
if (
|
||||
oldView.target === this.view?.target &&
|
||||
this.view.childIndex != oldView.childIndex
|
||||
) {
|
||||
const slide = this._getSlideForChild(this.view.childIndex);
|
||||
if (slide !== null && slide !== this.carouselSelected()) {
|
||||
if (
|
||||
slide !== null &&
|
||||
slide !== frigateCardCarousel.getCarouselSelected()?.index
|
||||
) {
|
||||
// If the media target is the same as already loaded, but isn't of
|
||||
// the selected slide, scroll to that slide.
|
||||
this.carouselScrollTo(slide);
|
||||
frigateCardCarousel.carouselScrollTo(slide);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,41 +221,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the media on the loaded slide.
|
||||
*/
|
||||
protected _autoPlayHandler(): void {
|
||||
if (
|
||||
this.viewerConfig?.auto_play &&
|
||||
['all', 'selected'].includes(this.viewerConfig.auto_play)
|
||||
) {
|
||||
super._autoPlayHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmute the media on the loaded slide.
|
||||
*/
|
||||
protected _autoUnmuteHandler(): void {
|
||||
if (
|
||||
this.viewerConfig?.auto_unmute &&
|
||||
['all', 'selected'].includes(this.viewerConfig.auto_unmute)
|
||||
) {
|
||||
super._autoUnmuteHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the carousel.
|
||||
*/
|
||||
protected _destroyCarousel(): void {
|
||||
super._destroyCarousel();
|
||||
|
||||
// Notes on instance variables:
|
||||
// * this._slideToChild: This is set as part of each render and does not
|
||||
// need to be destroyed here.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slide number given a media child number.
|
||||
* @param childIndex The child index (relative to `view.target`)
|
||||
@@ -265,8 +240,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* Get the transition effect to use.
|
||||
* @returns An TransitionEffect object.
|
||||
*/
|
||||
protected _getTransitionEffect(): TransitionEffect | undefined {
|
||||
return this.viewerConfig?.transition_effect;
|
||||
protected _getTransitionEffect(): TransitionEffect {
|
||||
return (
|
||||
this.viewerConfig?.transition_effect ??
|
||||
frigateCardConfigDefaults.media_viewer.transition_effect
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,16 +264,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
* @param slide An optional slide.
|
||||
* @returns The FrigateCardMediaPlayer or null if not found.
|
||||
*/
|
||||
protected _getPlayer(slide?: HTMLElement): FrigateCardMediaPlayer | null {
|
||||
if (this._carousel) {
|
||||
protected _getPlayer(slide?: HTMLElement | null): FrigateCardMediaPlayer | null {
|
||||
if (!slide) {
|
||||
slide = this._carousel.slideNodes()[this._carousel.selectedScrollSnap()];
|
||||
slide = this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.getCarouselSelected()?.element;
|
||||
}
|
||||
return slide?.querySelector(
|
||||
FRIGATE_CARD_HLS_SELECTOR,
|
||||
) as FrigateCardMediaPlayer | null;
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
(slide?.querySelector(FRIGATE_CARD_HLS_SELECTOR) as FrigateCardMediaPlayer) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +282,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
*/
|
||||
protected _getPlugins(): EmblaPluginType[] {
|
||||
return [
|
||||
...super._getPlugins(),
|
||||
// Only enable wheel plugin if there is more than one media item.
|
||||
...(this.view &&
|
||||
this.view.target &&
|
||||
@@ -480,15 +457,17 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
/**
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _selectSlideSetViewHandler(): void {
|
||||
if (!this._carousel || !this.view) {
|
||||
protected _setViewHandler(): void {
|
||||
if (!this._refMediaCarousel.value || !this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the childIndex in the view.
|
||||
const slidesInView = this._carousel.slidesInView(true);
|
||||
if (slidesInView.length) {
|
||||
const childIndex = this._slideToChild[slidesInView[0]];
|
||||
const selected = this._refMediaCarousel.value
|
||||
.frigateCardCarousel()
|
||||
?.getCarouselSelected()?.index;
|
||||
if (selected !== undefined) {
|
||||
const childIndex = this._slideToChild[selected];
|
||||
if (childIndex !== undefined) {
|
||||
this.view
|
||||
.evolve({
|
||||
@@ -556,31 +535,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle updating of the next/previous controls when the carousel is moved.
|
||||
*/
|
||||
protected _selectSlideNextPreviousHandler(): void {
|
||||
const updateNextPreviousControl = (
|
||||
control: FrigateCardNextPreviousControl,
|
||||
direction: 'previous' | 'next',
|
||||
): void => {
|
||||
const neighbors = this._getMediaNeighbors();
|
||||
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||
const target = direction == 'previous' ? prev : next;
|
||||
|
||||
control.disabled = target == null;
|
||||
control.title = target && target.title ? target.title : '';
|
||||
control.thumbnail = target && target.thumbnail ? target.thumbnail : undefined;
|
||||
};
|
||||
|
||||
if (this._previousControlRef.value) {
|
||||
updateNextPreviousControl(this._previousControlRef.value, 'previous');
|
||||
}
|
||||
if (this._nextControlRef.value) {
|
||||
updateNextPreviousControl(this._nextControlRef.value, 'next');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slides to include in the render.
|
||||
* @returns The slides to include in the render and an index keyed by slide
|
||||
@@ -647,59 +601,62 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
protected _render(): TemplateResult | void {
|
||||
const [slides, slideToChild] = this._getSlides();
|
||||
this._slideToChild = slideToChild;
|
||||
if (!slides.length) {
|
||||
if (!slides.length || !this.view?.media) {
|
||||
return;
|
||||
}
|
||||
|
||||
const neighbors = this._getMediaNeighbors();
|
||||
const [prev, next] = [neighbors?.previous, neighbors?.next];
|
||||
|
||||
return html`<div class="embla">
|
||||
// Notes on the below:
|
||||
// - guard() is used to avoid reseting the carousel unless the
|
||||
// options/plugins actually change.
|
||||
|
||||
return html` <frigate-card-media-carousel
|
||||
${ref(this._refMediaCarousel)}
|
||||
.carouselOptions=${guard([this.viewerConfig], this._getOptions.bind(this))}
|
||||
.carouselPlugins=${guard(
|
||||
[this.viewerConfig, this.view?.target?.children?.length],
|
||||
this._getPlugins.bind(this),
|
||||
) as EmblaCarouselPlugins}
|
||||
.label="${this.view.media.title}"
|
||||
.titlePopupConfig=${this.viewerConfig?.controls.title}
|
||||
transitionEffect=${this._getTransitionEffect()}
|
||||
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
|
||||
@frigate-card:media-show=${this._recordingSeekHandler.bind(this)}
|
||||
>
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._previousControlRef)}
|
||||
slot="previous"
|
||||
.direction=${'previous'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${prev && prev.thumbnail ? prev.thumbnail : undefined}
|
||||
.label=${prev ? prev.title : ''}
|
||||
?disabled=${!prev}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('previous');
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollPrevious();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
<div class="embla__viewport">
|
||||
<div class="embla__container">${slides}</div>
|
||||
</div>
|
||||
${slides}
|
||||
<frigate-card-next-previous-control
|
||||
${ref(this._nextControlRef)}
|
||||
slot="next"
|
||||
.direction=${'next'}
|
||||
.controlConfig=${this.viewerConfig?.controls.next_previous}
|
||||
.thumbnail=${next && next.thumbnail ? next.thumbnail : undefined}
|
||||
.label=${next ? next.title : ''}
|
||||
?disabled=${!next}
|
||||
@click=${(ev) => {
|
||||
this._nextPreviousHandler('next');
|
||||
this._refMediaCarousel.value?.frigateCardCarousel()?.carouselScrollNext();
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
></frigate-card-next-previous-control>
|
||||
</div>
|
||||
${this.view?.media
|
||||
? html` <frigate-card-title-control
|
||||
${ref(this._titleControlRef)}
|
||||
.config=${this.viewerConfig?.controls.title}
|
||||
.text="${this.view.media.title}"
|
||||
.fitInto=${this as HTMLElement}
|
||||
>
|
||||
</frigate-card-title-control>`
|
||||
: ``} `;
|
||||
</frigate-card-media-carousel>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected _selectSlideMediaShowHandler(): void {
|
||||
super._selectSlideMediaShowHandler();
|
||||
|
||||
protected _recordingSeekHandler(): void {
|
||||
// If this is a recording and play is desired to be started from a
|
||||
// particular point, seek to that point. Use the media off the slide itself
|
||||
// -- when the slide is changed, the media show event may be dispatched
|
||||
@@ -751,8 +708,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
)}
|
||||
.media=${mediaToRender}
|
||||
.hass=${this.hass}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) =>
|
||||
this._mediaShowEventHandler(slideIndex, e)}
|
||||
@frigate-card:media-show=${(e: CustomEvent<MediaShowInfo>) => {
|
||||
wrapMediaShowEventForCarousel(slideIndex, e);
|
||||
}}
|
||||
>
|
||||
</frigate-card-ha-hls-player>`
|
||||
: html`<img
|
||||
@@ -762,7 +720,11 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
)}
|
||||
title="${mediaToRender.title}"
|
||||
@click=${() => {
|
||||
if (this._carousel?.clickAllowed()) {
|
||||
if (
|
||||
this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.carouselClickAllowed()
|
||||
) {
|
||||
this._findRelatedClipView(mediaToRender).then((view) => {
|
||||
if (view) {
|
||||
view.dispatchChangeEvent(this);
|
||||
@@ -771,6 +733,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
}
|
||||
}}
|
||||
@load="${(e: Event) => {
|
||||
const lazyloadPlugin = this._refMediaCarousel.value
|
||||
?.frigateCardCarousel()
|
||||
?.getCarouselPlugins()?.lazyload;
|
||||
if (
|
||||
// This handler will be called on the empty image (including
|
||||
// an updated empty image that is the same dimensions large as
|
||||
@@ -778,22 +743,27 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
// images in media-carousel.ts). Here we need to only call the
|
||||
// media load handler on a 'real' load.
|
||||
!lazyLoad ||
|
||||
(this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded(
|
||||
slideIndex,
|
||||
)
|
||||
lazyloadPlugin?.hasLazyloaded(slideIndex)
|
||||
) {
|
||||
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
|
||||
wrapMediaLoadEventForCarousel(slideIndex, e);
|
||||
}
|
||||
}}"
|
||||
/>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(viewerCarouselStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"frigate-card-viewer-carousel": FrigateCardViewerCarousel
|
||||
"frigate-card-viewer": FrigateCardViewer
|
||||
'frigate-card-viewer-carousel': FrigateCardViewerCarousel;
|
||||
'frigate-card-viewer': FrigateCardViewer;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-20
@@ -4,16 +4,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img,video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.embla {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -28,10 +21,10 @@ img,video {
|
||||
-khtml-user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
:host([direction=vertical]) .embla__container {
|
||||
:host([direction='vertical']) .embla__container {
|
||||
flex-direction: column;
|
||||
}
|
||||
:host([direction=horizontal]) .embla__container {
|
||||
:host([direction='horizontal']) .embla__container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -53,18 +46,10 @@ img,video {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.embla__slide {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
:host([direction=vertical]) .embla__slide {
|
||||
:host([direction='vertical']) ::slotted(.embla__slide) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
:host([direction=horizontal]) .embla__slide {
|
||||
|
||||
:host([direction='horizontal']) ::slotted(.embla__slide) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.embla__slide img,video {
|
||||
// Letterbox media. <frigate-card-ha-hls-player> has similar added directly in
|
||||
// its element.
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.embla__slide {
|
||||
height: 100%;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
:host {
|
||||
--video-max-height: none;
|
||||
}
|
||||
|
||||
.embla__slide {
|
||||
flex: 0 0 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--video-max-height: none;
|
||||
|
||||
// Keep the controls relative to the media carousel itself.
|
||||
position: relative;
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.controls.icons {
|
||||
top: calc(50% - (40px / 2));
|
||||
top: calc(50% - (var(--frigate-card-next-prev-size) / 2));
|
||||
}
|
||||
|
||||
.controls.thumbnails {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@use 'const.scss';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
--frigate-card-carousel-thumbnail-opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
img,
|
||||
ha-icon {
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.embla__slide {
|
||||
height: 100%;
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.embla__slide img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Letterbox media. <frigate-card-ha-hls-player> has similar added directly in
|
||||
// its element.
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -1155,17 +1155,17 @@ electron-to-chromium@^1.4.147:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz#49cb5b35385bfee6cc439d0a04fbba7a7a7f08a1"
|
||||
integrity sha512-sTjBRhqh6wFodzZtc5Iu8/R95OkwaPNn7tj/TaDU5nu/5EFiQDtADGAXdR4tJcTEHlYfJpHqigzJqHvPgehP8A==
|
||||
|
||||
embla-carousel-wheel-gestures@^2.1.1:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-2.2.0.tgz#04ee1cfafe0667a5b96d16341642b3124fe8894e"
|
||||
integrity sha512-IoRGblg8QWrIgZEW0NbDcIl2fO++BLFf6197k2JNair3pfbyiMYtva6rROgeRiI0sIj2kFwlSEgudTy5f8TzNQ==
|
||||
embla-carousel-wheel-gestures@^3.0.0-rc01:
|
||||
version "3.0.0-rc01"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-3.0.0-rc01.tgz#70f88d6ee755817270ca26514d06b7c08c12977d"
|
||||
integrity sha512-h6E1/AwGKEwro8pey6KeOnt/UMvSaCwJKxaA+sz4OERPLmVL06oejVJ2kMjL06y9WaxZ8TqKWR7m0HlbsI9F5A==
|
||||
dependencies:
|
||||
wheel-gestures "^2.2.5"
|
||||
|
||||
embla-carousel@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-6.2.0.tgz#c16b18abe50e05ccd03d0b8d0b738f6a87aea1e0"
|
||||
integrity sha512-dSNsiQ7nmSQJZgbYfZCLdzrnznHwpaAcdJFcMRKgm/pjH1doOgxmfsvlMy4VfO4J11hLz8jm/W8WxSSDqfuu4w==
|
||||
embla-carousel@^7.0.0-rc05:
|
||||
version "7.0.0-rc05"
|
||||
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-7.0.0-rc05.tgz#0c70393cb9284435c8242d9ec9e5e754ef81749d"
|
||||
integrity sha512-zRPQniDxj3t3Q/okKzP8XsMRtANItAK7nhQ3Smpqfbtw3OMoZRebojUDCMD02sb5CY7ghYqL/XFEuXxS/7vJ5w==
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
||||
Reference in New Issue
Block a user