@@ -2,11 +2,11 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { getDiagnostics } from '../utils/diagnostics';
|
||||
import { DeviceRegistryManager } from '../utils/ha/registry/device';
|
||||
import { renderMessage } from './message';
|
||||
|
||||
@customElement('advanced-camera-card-diagnostics')
|
||||
|
||||
@@ -64,9 +64,6 @@ export class AdvancedCameraCardDrawer extends LitElement {
|
||||
this._refDrawer.value?.shadowRoot?.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the slotted children in the drawer change.
|
||||
*/
|
||||
protected _slotChanged(): void {
|
||||
const children = this._refSlot.value
|
||||
? getChildrenFromElement(this._refSlot.value)
|
||||
@@ -80,10 +77,6 @@ export class AdvancedCameraCardDrawer extends LitElement {
|
||||
this._hideDrawerIfNecessary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the drawer if there is nothing to show.
|
||||
* @returns
|
||||
*/
|
||||
protected _hideDrawerIfNecessary(): void {
|
||||
if (!this._refDrawer.value) {
|
||||
return;
|
||||
@@ -108,9 +101,7 @@ export class AdvancedCameraCardDrawer extends LitElement {
|
||||
location="${this.location}"
|
||||
?open=${this.open}
|
||||
@mouseleave=${() => {
|
||||
if (this.open) {
|
||||
this.open = false;
|
||||
}
|
||||
this.open = false;
|
||||
}}
|
||||
>
|
||||
${this.control
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
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 throttle from 'lodash-es/throttle';
|
||||
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
|
||||
import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { GalleryConfig } from '../config/schema/gallery.js';
|
||||
import { CardWideConfig, configDefaults } from '../config/schema/types.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import { localize } from '../localize/localize';
|
||||
import galleryCoreStyle from '../scss/gallery-core.scss';
|
||||
import galleryStyle from '../scss/gallery.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { errorToConsole, sleep } from '../utils/basic';
|
||||
import { scrollIntoView } from '../utils/scroll.js';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import './media-filter';
|
||||
import './message.js';
|
||||
import { renderMessage } from './message.js';
|
||||
import './progress-indicator.js';
|
||||
import { renderProgressIndicator } from './progress-indicator.js';
|
||||
import './surround-basic';
|
||||
import './thumbnail.js';
|
||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||
|
||||
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
|
||||
closed: 'mdi:filter-cog-outline',
|
||||
open: 'mdi:filter-cog',
|
||||
};
|
||||
|
||||
const MIN_GALLERY_EXTENSION_SECONDS = 0.5;
|
||||
|
||||
@customElement('advanced-camera-card-gallery')
|
||||
export class AdvancedCameraCardGallery extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public galleryConfig?: GalleryConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
if (
|
||||
!this.hass ||
|
||||
!view?.isGalleryView() ||
|
||||
!this.cameraManager ||
|
||||
!this.cardWideConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return html`
|
||||
<advanced-camera-card-surround-basic
|
||||
.drawerIcons=${{
|
||||
...(this.galleryConfig &&
|
||||
this.galleryConfig.controls.filter.mode !== 'none' && {
|
||||
[this.galleryConfig.controls.filter.mode]: GALLERY_MEDIA_FILTER_MENU_ICONS,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
|
||||
? html` <advanced-camera-card-media-filter
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
slot=${this.galleryConfig.controls.filter.mode}
|
||||
>
|
||||
</advanced-camera-card-media-filter>`
|
||||
: ''}
|
||||
<advanced-camera-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.galleryConfig=${this.galleryConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</advanced-camera-card-gallery-core>
|
||||
</advanced-camera-card-surround-basic>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(galleryStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-gallery-core')
|
||||
export class AdvancedCameraCardGalleryCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public galleryConfig?: GalleryConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
protected _resizeObserver: ResizeObserver;
|
||||
protected _refLoaderBottom: Ref<HTMLElement> = createRef();
|
||||
protected _refSelected: Ref<HTMLElement> = createRef();
|
||||
|
||||
// Bottom loader: A progress indicator shown in a "cell" (not across) at the
|
||||
// bottom of the gallery. Once visible this attempts to fetch new content from
|
||||
// "earlier" (less recently) than the current query. This is rendered by
|
||||
// default (and once visible, the fetch is triggered after which it is
|
||||
// re-hidden).
|
||||
@state()
|
||||
protected _showLoaderBottom = true;
|
||||
|
||||
// Top loader: A progress indicator is shown across the top of the gallery if
|
||||
// the user is _already_ at the top of the gallery and scrolls upwards. This
|
||||
// attempts to fetch new content from "later" (more recently) than the current
|
||||
// query. This is hidden by default.
|
||||
@state()
|
||||
protected _showLoaderTop = false;
|
||||
|
||||
protected _media?: ViewMedia[];
|
||||
|
||||
protected _boundWheelHandler = this._wheelHandler.bind(this);
|
||||
protected _boundTouchStartHandler = this._touchStartHandler.bind(this);
|
||||
protected _boundTouchEndHandler = this._touchEndHandler.bind(this);
|
||||
|
||||
// Wheel / touch events may be voluminous, throttle extension calls.
|
||||
protected _throttleExtendGalleryLater = throttle(
|
||||
this._extendGallery.bind(this),
|
||||
MIN_GALLERY_EXTENSION_SECONDS * 1000,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false,
|
||||
},
|
||||
);
|
||||
|
||||
protected _touchScrollYPosition: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._resizeObserver = new ResizeObserver(this._resizeHandler.bind(this));
|
||||
this._intersectionObserver = new IntersectionObserver(
|
||||
this._intersectionHandler.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
// Since the scroll event does not fire if the user is already at the top of
|
||||
// the container, instead we manually use the wheel and touchstart/end events
|
||||
// to detect "top upwards scrolling" (to trigger an extension of the gallery).
|
||||
|
||||
protected _touchStartHandler(ev: TouchEvent): void {
|
||||
// Remember the Y touch position on touch start, so that we can calculate if
|
||||
// the user gestured upwards or downards on touchend.
|
||||
if (ev.touches.length === 1) {
|
||||
this._touchScrollYPosition = ev.touches[0].screenY;
|
||||
} else {
|
||||
this._touchScrollYPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async _touchEndHandler(ev: TouchEvent): Promise<void> {
|
||||
if (
|
||||
!this.scrollTop &&
|
||||
ev.changedTouches.length === 1 &&
|
||||
this._touchScrollYPosition
|
||||
) {
|
||||
if (ev.changedTouches[0].screenY > this._touchScrollYPosition) {
|
||||
await this._extendLater();
|
||||
}
|
||||
}
|
||||
this._touchScrollYPosition = null;
|
||||
}
|
||||
|
||||
protected async _wheelHandler(ev: WheelEvent): Promise<void> {
|
||||
if (!this.scrollTop && ev.deltaY < 0) {
|
||||
await this._extendLater();
|
||||
}
|
||||
}
|
||||
|
||||
protected async _extendLater(): Promise<void> {
|
||||
const start = new Date();
|
||||
this._showLoaderTop = true;
|
||||
await this._throttleExtendGalleryLater(
|
||||
'later',
|
||||
// Ask the engine to avoid use of cache since the user is explicitly
|
||||
// looking for the freshest possible data.
|
||||
false,
|
||||
);
|
||||
const delta = new Date().getTime() - start.getTime();
|
||||
if (delta < MIN_GALLERY_EXTENSION_SECONDS * 1000) {
|
||||
// Hidden gem: "legitimate" (?!) use of sleep() :-)
|
||||
// These calls can return very quickly even with caching disabled since
|
||||
// the time window constraints on the query will usually be very narrow
|
||||
// and the backend can thus very quickly reply. It's often so fast it
|
||||
// actually looks like a rendering issue where the progress indictor
|
||||
// barely registers before it's gone again. This optional pause ensures
|
||||
// there is at least some visual feedback to the user that last long
|
||||
// enough they can 'feel' the fetch has happened.
|
||||
await sleep(MIN_GALLERY_EXTENSION_SECONDS - delta / 1000);
|
||||
}
|
||||
this._showLoaderTop = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._resizeObserver.observe(this);
|
||||
this.addEventListener('wheel', this._boundWheelHandler, { passive: true });
|
||||
this.addEventListener('touchstart', this._boundTouchStartHandler, { passive: true });
|
||||
this.addEventListener('touchend', this._boundTouchEndHandler);
|
||||
|
||||
// Request update in order to ensure the intersection observer reconnects
|
||||
// with the loader sentinel.
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener('wheel', this._boundWheelHandler);
|
||||
this.removeEventListener('touchstart', this._boundTouchStartHandler);
|
||||
this.removeEventListener('touchend', this._boundTouchEndHandler);
|
||||
this._resizeObserver.disconnect();
|
||||
this._intersectionObserver.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gallery columns.
|
||||
*/
|
||||
protected _setColumnCount(): void {
|
||||
const thumbnailSize =
|
||||
this.galleryConfig?.controls.thumbnails.size ??
|
||||
configDefaults.media_gallery.controls.thumbnails.size;
|
||||
const columns = this.galleryConfig?.controls.thumbnails.show_details
|
||||
? Math.max(1, Math.floor(this.clientWidth / THUMBNAIL_DETAILS_WIDTH_MIN))
|
||||
: Math.max(1, Math.ceil(this.clientWidth / thumbnailSize));
|
||||
|
||||
this.style.setProperty('--advanced-camera-card-gallery-columns', String(columns));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle gallery resize.
|
||||
*/
|
||||
protected _resizeHandler(): void {
|
||||
this._setColumnCount();
|
||||
}
|
||||
|
||||
protected async _intersectionHandler(
|
||||
entries: IntersectionObserverEntry[],
|
||||
): Promise<void> {
|
||||
if (entries.every((entry) => !entry.isIntersecting)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showLoaderBottom = false;
|
||||
await this._extendGallery('earlier');
|
||||
}
|
||||
|
||||
protected async _extendGallery(
|
||||
direction: 'earlier' | 'later',
|
||||
useCache = true,
|
||||
): Promise<void> {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
|
||||
if (!this.cameraManager || !this.hass || !view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = view.query;
|
||||
const rawQueries = query?.getQueries() ?? null;
|
||||
const existingMedia = view.queryResults?.getResults();
|
||||
if (!query || !rawQueries || !existingMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
let extension: ExtendedMediaQueryResult<MediaQuery> | null;
|
||||
try {
|
||||
extension = await this.cameraManager.extendMediaQueries<MediaQuery>(
|
||||
rawQueries,
|
||||
existingMedia,
|
||||
direction,
|
||||
{
|
||||
useCache: useCache,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension) {
|
||||
const newMediaQueries = MediaQueriesClassifier.areEventQueries(query)
|
||||
? new EventMediaQueries(extension.queries as EventQuery[])
|
||||
: MediaQueriesClassifier.areRecordingQueries(query)
|
||||
? new RecordingMediaQueries(extension.queries as RecordingQuery[])
|
||||
: null;
|
||||
|
||||
if (newMediaQueries) {
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
baseView: view,
|
||||
params: {
|
||||
query: newMediaQueries,
|
||||
queryResults: new MediaQueriesResults({
|
||||
results: extension.results,
|
||||
}).selectResultIfFound(
|
||||
(media) => media === view.queryResults?.getSelectedResult(),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an update will occur.
|
||||
* @param changedProps The changed properties
|
||||
*/
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('galleryConfig')) {
|
||||
if (this.galleryConfig?.controls.thumbnails.show_details) {
|
||||
this.setAttribute('details', '');
|
||||
} else {
|
||||
this.removeAttribute('details');
|
||||
}
|
||||
this._setColumnCount();
|
||||
if (this.galleryConfig?.controls.thumbnails.size) {
|
||||
this.style.setProperty(
|
||||
'--advanced-camera-card-thumbnail-size',
|
||||
`${this.galleryConfig.controls.thumbnails.size}px`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (changedProps.has('viewManagerEpoch')) {
|
||||
// If the view changes, always render the bottom loader to allow for the
|
||||
// view to be extended once the bottom loader becomes visible.
|
||||
this._showLoaderBottom = true;
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const oldView = this.viewManagerEpoch?.oldView;
|
||||
if (
|
||||
!this._media ||
|
||||
oldView?.queryResults?.getResults() !== view?.queryResults?.getResults()
|
||||
) {
|
||||
// Gallery places the most recent media at the top (the query results place
|
||||
// the most recent media at the end for use in the viewer). This is copied
|
||||
// to a new array to avoid reversing the query results in place.
|
||||
this._media = [...(view?.queryResults?.getResults() ?? [])].reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._media || !this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
if (!view?.queryResults || view.queryResults.getResultsCount() === 0) {
|
||||
// Note that this is not throwing up an error message for the card to
|
||||
// handle (as typical), but rather directly rendering the message into the
|
||||
// gallery. This is to allow the filter to still be available when a given
|
||||
// filter selection returns no media.
|
||||
const loadingMedia = !!view?.context?.loading?.query;
|
||||
return renderMessage({
|
||||
type: 'info',
|
||||
message: loadingMedia
|
||||
? localize('error.awaiting_media')
|
||||
: localize('common.no_media'),
|
||||
icon: 'mdi:multimedia',
|
||||
dotdotdot: loadingMedia,
|
||||
});
|
||||
}
|
||||
|
||||
const selected = view.queryResults.getSelectedResult();
|
||||
return html` <div class="grid">
|
||||
${this._showLoaderTop
|
||||
? html`${renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
classes: {
|
||||
top: true,
|
||||
},
|
||||
size: 'small',
|
||||
})}`
|
||||
: ''}
|
||||
${this._media.map(
|
||||
(media, index) =>
|
||||
html`<advanced-camera-card-thumbnail
|
||||
${media === selected ? ref(this._refSelected) : ''}
|
||||
class=${classMap({
|
||||
selected: media === selected,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.media=${media}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_favorite_control}
|
||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_timeline_control}
|
||||
?show_download_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_download_control}
|
||||
@click=${(ev: Event) => {
|
||||
if (this._media) {
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
params: {
|
||||
view: 'media',
|
||||
queryResults: view.queryResults?.clone().selectIndex(
|
||||
// Media in the gallery is reversed vs the queryResults (see
|
||||
// note above).
|
||||
this._media.length - index - 1,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</advanced-camera-card-thumbnail>`,
|
||||
)}
|
||||
${this._showLoaderBottom
|
||||
? html`${renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
componentRef: this._refLoaderBottom,
|
||||
})}`
|
||||
: ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
public updated(changedProps: PropertyValues): void {
|
||||
if (this._refLoaderBottom.value) {
|
||||
this._intersectionObserver.disconnect();
|
||||
this._intersectionObserver.observe(this._refLoaderBottom.value);
|
||||
}
|
||||
|
||||
// This wait for updateComplete is necessary for the scrolling to work
|
||||
// correctly.
|
||||
this.updateComplete.then(() => {
|
||||
// As a special case, if the view has changed and did not previously exist
|
||||
// (i.e. first setting of it), we intentionally scroll the gallery to the
|
||||
// selected element in that view (if any).
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/885
|
||||
if (
|
||||
// If this update cycle updated the view ...
|
||||
changedProps.has('viewManagerEpoch') &&
|
||||
// ... and it wasn't set at all prior ...
|
||||
!changedProps.get('viewManagerEpoch') &&
|
||||
// ... and there is a thumbnail rendered that is selected.
|
||||
this._refSelected.value
|
||||
) {
|
||||
scrollIntoView(this._refSelected.value, {
|
||||
boundary: this,
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(galleryCoreStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-gallery-core': AdvancedCameraCardGalleryCore;
|
||||
'advanced-camera-card-gallery': AdvancedCameraCardGallery;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ViewItemManager } from '../../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import {
|
||||
getUpFolderMediaItem,
|
||||
upFolderClickHandler,
|
||||
} from '../../components-lib/folder/up-folder.js';
|
||||
import { FolderGalleryController } from '../../components-lib/gallery/folder-gallery-controller.js';
|
||||
import { MediaGalleryConfig } from '../../config/schema/media-gallery.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize';
|
||||
import folderGalleryStyle from '../../scss/folder-gallery.scss';
|
||||
import { ViewItem } from '../../view/item.js';
|
||||
import '../media-filter';
|
||||
import '../message.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../surround-basic';
|
||||
import '../thumbnail/thumbnail.js';
|
||||
import './gallery-core.js';
|
||||
|
||||
@customElement('advanced-camera-card-folder-gallery')
|
||||
export class AdvancedCameraCardFolderGallery extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public galleryConfig?: MediaGalleryConfig;
|
||||
|
||||
protected _controller = new FolderGalleryController(this);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('galleryConfig')) {
|
||||
this._controller.setThumbnailSize(this.galleryConfig?.controls.thumbnails.size);
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderThumbnail(
|
||||
item: ViewItem,
|
||||
selected: boolean,
|
||||
clickCallback: (item: ViewItem, ev: Event) => void,
|
||||
): TemplateResult | void {
|
||||
return html`<advanced-camera-card-thumbnail
|
||||
class=${classMap({
|
||||
selected,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.item=${item}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
?selected=${selected}
|
||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_favorite_control}
|
||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_timeline_control}
|
||||
?show_download_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_download_control}
|
||||
@click=${(ev: Event) => clickCallback(item, ev)}
|
||||
>
|
||||
</advanced-camera-card-thumbnail>`;
|
||||
}
|
||||
|
||||
protected _renderThumbnails(): TemplateResult | void {
|
||||
const selected = this.viewManagerEpoch?.manager
|
||||
.getView()
|
||||
?.queryResults?.getSelectedResult();
|
||||
|
||||
return html`
|
||||
${this.viewManagerEpoch?.manager
|
||||
.getView()
|
||||
?.queryResults?.getResults()
|
||||
?.map((item) =>
|
||||
this._renderThumbnail(item, item === selected, (item: ViewItem, ev: Event) => {
|
||||
const manager = this.viewManagerEpoch?.manager;
|
||||
if (manager) {
|
||||
this._controller.itemClickHandler(manager, item, ev);
|
||||
}
|
||||
}),
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const folderIsLoading =
|
||||
!!this.viewManagerEpoch?.manager.getView()?.context?.loading?.query;
|
||||
const upThumbnail = getUpFolderMediaItem(this.viewManagerEpoch?.manager.getView());
|
||||
|
||||
return html`
|
||||
<advanced-camera-card-surround-basic>
|
||||
${!this.viewManagerEpoch?.manager.getView()?.queryResults?.hasResults() &&
|
||||
(folderIsLoading || !upThumbnail)
|
||||
? renderMessage({
|
||||
type: 'info',
|
||||
message: folderIsLoading
|
||||
? localize('error.awaiting_folder')
|
||||
: localize('common.no_folder'),
|
||||
icon: 'mdi:folder-play',
|
||||
dotdotdot: folderIsLoading,
|
||||
})
|
||||
: html`<advanced-camera-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
.columnWidth=${this._controller.getColumnWidth(
|
||||
this.galleryConfig?.controls.thumbnails,
|
||||
)}
|
||||
.columnCountRoundMethod=${this._controller.getColumnCountRoundMethod(
|
||||
this.galleryConfig?.controls.thumbnails,
|
||||
)}
|
||||
>
|
||||
${upThumbnail
|
||||
? this._renderThumbnail(
|
||||
upThumbnail,
|
||||
false,
|
||||
(item: ViewItem, ev: Event) =>
|
||||
upFolderClickHandler(item, ev, this.viewManagerEpoch),
|
||||
)
|
||||
: ''}
|
||||
${this._renderThumbnails()}
|
||||
</advanced-camera-card-gallery-core>`}
|
||||
</advanced-camera-card-surround-basic>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(folderGalleryStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-folder-gallery': AdvancedCameraCardFolderGallery;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import {
|
||||
GalleryColumnCountRoundMethod,
|
||||
GalleryCoreController,
|
||||
} from '../../components-lib/gallery/gallery-core-controller.js';
|
||||
import { THUMBNAIL_WIDTH_DEFAULT } from '../../config/schema/common/controls/thumbnails.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import galleryCoreStyle from '../../scss/gallery-core.scss';
|
||||
import '../message.js';
|
||||
import '../progress-indicator.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
|
||||
@customElement('advanced-camera-card-gallery-core')
|
||||
export class AdvancedCameraCardGalleryCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public columnWidth: number = THUMBNAIL_WIDTH_DEFAULT;
|
||||
|
||||
@property({ attribute: false })
|
||||
public columnCountRoundMethod?: GalleryColumnCountRoundMethod;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public extendUp = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public extendDown = false;
|
||||
|
||||
private _refSentinelBottom: Ref<HTMLElement> = createRef();
|
||||
private _refSlot: Ref<HTMLSlotElement> = createRef();
|
||||
|
||||
private _controller = new GalleryCoreController(
|
||||
this,
|
||||
() => this._refSlot.value ?? null,
|
||||
() => this._refSentinelBottom.value ?? null,
|
||||
(show) => {
|
||||
this._showLoaderTop = show;
|
||||
},
|
||||
(show) => {
|
||||
this._showSentinelBottom = show;
|
||||
},
|
||||
);
|
||||
|
||||
// Top loader: A progress indicator is shown across the top of the gallery if
|
||||
// the user is _already_ at the top of the gallery and scrolls upwards. This
|
||||
// attempts to fetch new content from "later" (more recently) than the current
|
||||
// query. This is hidden by default.
|
||||
@state()
|
||||
private _showLoaderTop = false;
|
||||
|
||||
// Bottom sentinel: A progress indicator shown in a "cell" (not across) at the
|
||||
// bottom of the gallery. Once visible an attempt is optionally made to extend
|
||||
// the gallery downwards. This is rendered by default (so intersection can be
|
||||
// detected), and hidden during fetches.
|
||||
@state()
|
||||
private _showSentinelBottom = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
['columnCountRoundMethod', 'columnWidth', 'extendUp', 'extendDown'].some((prop) =>
|
||||
changedProps.has(prop),
|
||||
)
|
||||
) {
|
||||
this._controller.setOptions({
|
||||
extendUp: this.extendUp,
|
||||
extendDown: this.extendDown,
|
||||
columnWidth: this.columnWidth,
|
||||
columnCountRoundMethod: this.columnCountRoundMethod,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html` <div class="grid">
|
||||
${this.extendUp && this._showLoaderTop
|
||||
? html`${renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
classes: {
|
||||
top: true,
|
||||
},
|
||||
size: 'small',
|
||||
})}`
|
||||
: ''}
|
||||
<slot ${ref(this._refSlot)} @slotchange=${() => this._controller.updateContents()}>
|
||||
</slot>
|
||||
${this.extendDown && this._showSentinelBottom
|
||||
? html`${renderProgressIndicator({
|
||||
cardWideConfig: this.cardWideConfig,
|
||||
componentRef: this._refSentinelBottom,
|
||||
size: 'small',
|
||||
})}`
|
||||
: ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(galleryCoreStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-gallery-core': AdvancedCameraCardGalleryCore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { ViewItemManager } from '../../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { MediaGalleryController } from '../../components-lib/gallery/media-gallery-controller.js';
|
||||
import { MediaGalleryConfig } from '../../config/schema/media-gallery.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize';
|
||||
import mediaGalleryStyle from '../../scss/media-gallery.scss';
|
||||
import '../media-filter';
|
||||
import '../message.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../surround-basic';
|
||||
import '../thumbnail/thumbnail.js';
|
||||
import './gallery-core.js';
|
||||
import { GalleryExtendEvent } from './types.js';
|
||||
|
||||
const MEDIA_GALLERY_FILTER_MENU_ICONS = {
|
||||
closed: 'mdi:filter-cog-outline',
|
||||
open: 'mdi:filter-cog',
|
||||
};
|
||||
|
||||
@customElement('advanced-camera-card-media-gallery')
|
||||
export class AdvancedCameraCardMediaGallery extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public galleryConfig?: MediaGalleryConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _controller = new MediaGalleryController(this);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('viewManagerEpoch')) {
|
||||
this._controller.setMediaFromView(
|
||||
this.viewManagerEpoch?.manager.getView(),
|
||||
this.viewManagerEpoch?.oldView,
|
||||
);
|
||||
}
|
||||
|
||||
if (changedProps.has('galleryConfig')) {
|
||||
this._controller.setThumbnailSize(this.galleryConfig?.controls.thumbnails.size);
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderThumbnails(): TemplateResult | void {
|
||||
const selected = this.viewManagerEpoch?.manager
|
||||
.getView()
|
||||
?.queryResults?.getSelectedResult();
|
||||
|
||||
return html`
|
||||
${this._controller.getMedia()?.map(
|
||||
(media, index) =>
|
||||
html`<advanced-camera-card-thumbnail
|
||||
class=${classMap({
|
||||
selected: media === selected,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.item=${media}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
?selected=${media === selected}
|
||||
?details=${!!this.galleryConfig?.controls.thumbnails.show_details}
|
||||
?show_favorite_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_favorite_control}
|
||||
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_timeline_control}
|
||||
?show_download_control=${!!this.galleryConfig?.controls.thumbnails
|
||||
.show_download_control}
|
||||
@click=${(ev: Event) => {
|
||||
const manager = this.viewManagerEpoch?.manager;
|
||||
if (manager) {
|
||||
this._controller.itemClickHandler(manager, index, ev);
|
||||
}
|
||||
}}
|
||||
>
|
||||
</advanced-camera-card-thumbnail>`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const mediaIsLoading =
|
||||
!!this.viewManagerEpoch?.manager.getView()?.context?.loading?.query;
|
||||
|
||||
return html`
|
||||
<advanced-camera-card-surround-basic
|
||||
.drawerIcons=${{
|
||||
...(this.galleryConfig &&
|
||||
this.galleryConfig.controls.filter.mode !== 'none' && {
|
||||
[this.galleryConfig.controls.filter.mode]: MEDIA_GALLERY_FILTER_MENU_ICONS,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
|
||||
? html` <advanced-camera-card-media-filter
|
||||
.hass=${this.hass}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
slot=${this.galleryConfig.controls.filter.mode}
|
||||
>
|
||||
</advanced-camera-card-media-filter>`
|
||||
: ''}
|
||||
${!this._controller.getMedia()?.length
|
||||
? renderMessage({
|
||||
type: 'info',
|
||||
message: mediaIsLoading
|
||||
? localize('error.awaiting_media')
|
||||
: localize('common.no_media'),
|
||||
icon: 'mdi:multimedia',
|
||||
dotdotdot: mediaIsLoading,
|
||||
})
|
||||
: html`<advanced-camera-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
.columnWidth=${this._controller.getColumnWidth(
|
||||
this.galleryConfig?.controls.thumbnails,
|
||||
)}
|
||||
.columnCountRoundMethod=${this._controller.getColumnCountRoundMethod(
|
||||
this.galleryConfig?.controls.thumbnails,
|
||||
)}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.extendUp=${true}
|
||||
.extendDown=${true}
|
||||
@advanced-camera-card:gallery:extend:up=${(
|
||||
ev: CustomEvent<GalleryExtendEvent>,
|
||||
) =>
|
||||
this._extendGallery(
|
||||
ev,
|
||||
'later',
|
||||
// Avoid use of cache since the user is explicitly looking for
|
||||
// the freshest possible data.
|
||||
false,
|
||||
)}
|
||||
@advanced-camera-card:gallery:extend:down=${(
|
||||
ev: CustomEvent<GalleryExtendEvent>,
|
||||
) => this._extendGallery(ev, 'earlier')}
|
||||
>
|
||||
${this._renderThumbnails()}
|
||||
</advanced-camera-card-gallery-core>`}
|
||||
</advanced-camera-card-surround-basic>
|
||||
`;
|
||||
}
|
||||
|
||||
protected async _extendGallery(
|
||||
ev: CustomEvent<GalleryExtendEvent>,
|
||||
direction: 'earlier' | 'later',
|
||||
useCache = true,
|
||||
): Promise<void> {
|
||||
if (!this.cameraManager || !this.viewManagerEpoch) {
|
||||
return;
|
||||
}
|
||||
await this._controller.extendMediaGallery(
|
||||
this.cameraManager,
|
||||
this.viewManagerEpoch,
|
||||
direction,
|
||||
useCache,
|
||||
);
|
||||
ev.detail.resolve();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(mediaGalleryStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-media-gallery': AdvancedCameraCardMediaGallery;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface GalleryExtendEvent {
|
||||
resolve: () => void;
|
||||
}
|
||||
@@ -4,7 +4,12 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { ImageMediaPlayerController } from '../components-lib/media-player/image';
|
||||
import imagePlayerStyle from '../scss/image-player.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types';
|
||||
import {
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
MediaTechnology,
|
||||
} from '../types';
|
||||
import { dispatchMediaLoadedEvent } from '../utils/media-info';
|
||||
|
||||
/**
|
||||
@@ -16,7 +21,7 @@ export class AdvancedCameraCardImagePlayer extends LitElement implements MediaPl
|
||||
public url?: string;
|
||||
|
||||
@property()
|
||||
public filetype?: string;
|
||||
public technology?: MediaTechnology;
|
||||
|
||||
protected _refImage: Ref<MediaPlayerElement<HTMLImageElement>> = createRef();
|
||||
protected _mediaPlayerController = new ImageMediaPlayerController(
|
||||
@@ -37,7 +42,7 @@ export class AdvancedCameraCardImagePlayer extends LitElement implements MediaPl
|
||||
...(this._mediaPlayerController && {
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
}),
|
||||
technology: [this.filetype ?? 'jpg'],
|
||||
technology: [this.technology ?? ('jpg' as const)],
|
||||
});
|
||||
}}
|
||||
/>`;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js';
|
||||
import { CachedValueController } from '../components-lib/cached-value-controller.js';
|
||||
@@ -18,6 +18,7 @@ import { UpdatingImageMediaPlayerController } from '../components-lib/media-play
|
||||
import { CameraConfig } from '../config/schema/cameras.js';
|
||||
import { ImageMode } from '../config/schema/common/image.js';
|
||||
import { ImageViewConfig } from '../config/schema/image.js';
|
||||
import { isHassDifferent } from '../ha/is-hass-different.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import defaultImage from '../images/iris-screensaver.jpg';
|
||||
import { localize } from '../localize/localize.js';
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
Message,
|
||||
} from '../types.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import { isHassDifferent } from '../utils/ha/index.js';
|
||||
import {
|
||||
createMediaLoadedInfo,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
|
||||
@@ -380,7 +380,10 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
type: 'error',
|
||||
icon: 'mdi:camera-off',
|
||||
message: localize('error.stream_not_loading'),
|
||||
troubleshootingURL: STREAM_TROUBLESHOOTING_URL,
|
||||
url: {
|
||||
link: STREAM_TROUBLESHOOTING_URL,
|
||||
title: localize('error.troubleshooting'),
|
||||
},
|
||||
},
|
||||
{ overlay: true },
|
||||
)
|
||||
|
||||
@@ -74,11 +74,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSMPEG player.
|
||||
* @param url The URL for the player to connect to.
|
||||
* @returns A JSMPEG player.
|
||||
*/
|
||||
protected async _createJSMPEGPlayer(url: string): Promise<JSMpeg.VideoElement> {
|
||||
this._jsmpegVideoPlayer = await new Promise<JSMpeg.VideoElement>((resolve) => {
|
||||
let videoDecoded = false;
|
||||
@@ -136,16 +131,14 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset / destroy the player.
|
||||
*/
|
||||
protected _resetPlayer(): void {
|
||||
this._message = null;
|
||||
this._refreshPlayerTimer.stop();
|
||||
if (this._jsmpegVideoPlayer) {
|
||||
try {
|
||||
this._jsmpegVideoPlayer.destroy();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
// Pass.
|
||||
}
|
||||
this._jsmpegVideoPlayer = undefined;
|
||||
@@ -156,9 +149,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.isConnected) {
|
||||
@@ -166,9 +156,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
if (!this.isConnected) {
|
||||
this._resetPlayer();
|
||||
@@ -176,9 +163,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the JSMPEG player.
|
||||
*/
|
||||
protected async _refreshPlayer(): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
@@ -221,9 +205,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (this._message) {
|
||||
return renderMessage(this._message);
|
||||
@@ -253,9 +234,6 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get styles.
|
||||
*/
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(liveJSMPEGStyle);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,6 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
* Create the WebRTC element. May throw.
|
||||
*/
|
||||
protected _createWebRTC(): HTMLElement | null {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const webrtcElement = this._webrtcTask.value;
|
||||
if (webrtcElement && this.hass && this.cameraConfig) {
|
||||
const webrtc = new webrtcElement() as HTMLElement & {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { actionHandler } from '../action-handler-directive.js';
|
||||
import { MenuController } from '../components-lib/menu-controller.js';
|
||||
import { MenuItem } from '../config/schema/elements/custom/menu/types.js';
|
||||
import { MenuConfig } from '../config/schema/menu.js';
|
||||
import { getEntityTitle } from '../ha/get-entity-title.js';
|
||||
import { EntityRegistryManager } from '../ha/registry/entity/types.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import menuStyle from '../scss/menu.scss';
|
||||
import { hasAction } from '../utils/action.js';
|
||||
import { getEntityTitle } from '../utils/ha/index.js';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types.js';
|
||||
import './icon.js';
|
||||
import './submenu/select-button.js';
|
||||
import './submenu/submenu-button';
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { MessageController } from '../components-lib/message/controller.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import messageStyle from '../scss/message.scss';
|
||||
import { Message } from '../types.js';
|
||||
import './icon.js';
|
||||
@@ -33,14 +32,10 @@ export class AdvancedCameraCardMessage extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = this._controller.getURL(this.message);
|
||||
const messageTemplate = html`
|
||||
${this._controller.getMessageString(this.message)}
|
||||
${this._controller.shouldShowTroubleshootingURL(this.message)
|
||||
? html`.
|
||||
<a href="${this._controller.getTroubleshootingURL(this.message)}"
|
||||
>${localize('error.troubleshooting')}</a
|
||||
>`
|
||||
: ''}
|
||||
${url ? html`. <a href="${url.link}">${url.title}</a>` : ''}
|
||||
`;
|
||||
|
||||
const icon = this._controller.getIcon(this.message);
|
||||
@@ -52,6 +47,7 @@ export class AdvancedCameraCardMessage extends LitElement {
|
||||
<div class="message padded">
|
||||
<div class="icon">
|
||||
<advanced-camera-card-icon
|
||||
part="icon"
|
||||
.icon="${{ icon: icon }}"
|
||||
></advanced-camera-card-icon>
|
||||
</div>
|
||||
|
||||
@@ -41,9 +41,6 @@ export class AdvancedCameraCardNextPreviousControl extends LitElement {
|
||||
// Label that is used for ARIA support and as tooltip.
|
||||
@property() label = '';
|
||||
|
||||
@state()
|
||||
protected _thumbnailError = false;
|
||||
|
||||
protected _embedThumbnailTask = createFetchThumbnailTask(
|
||||
this,
|
||||
() => this.hass,
|
||||
@@ -55,35 +52,45 @@ export class AdvancedCameraCardNextPreviousControl extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const renderIcon =
|
||||
!this.thumbnail ||
|
||||
['chevrons', 'icons'].includes(this._controlConfig.style) ||
|
||||
this._thumbnailError;
|
||||
const shouldRenderIcon =
|
||||
!this.thumbnail || ['chevrons', 'icons'].includes(this._controlConfig.style);
|
||||
|
||||
const classes = {
|
||||
const classesBase = {
|
||||
controls: true,
|
||||
left: this.side === 'left',
|
||||
right: this.side === 'right',
|
||||
thumbnails: !renderIcon,
|
||||
icons: renderIcon,
|
||||
};
|
||||
|
||||
if (renderIcon) {
|
||||
const renderIcon = (): TemplateResult => {
|
||||
const icon =
|
||||
this.icon && !this._thumbnailError && this._controlConfig.style !== 'chevrons'
|
||||
this.icon && this._controlConfig?.style !== 'chevrons'
|
||||
? this.icon
|
||||
: this.side === 'left'
|
||||
? { icon: 'mdi:chevron-left' }
|
||||
: { icon: 'mdi:chevron-right' };
|
||||
|
||||
const classes = {
|
||||
...classesBase,
|
||||
icons: true,
|
||||
};
|
||||
|
||||
return html` <ha-icon-button class="${classMap(classes)}" .label=${this.label}>
|
||||
<advanced-camera-card-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${icon}
|
||||
></advanced-camera-card-icon>
|
||||
</ha-icon-button>`;
|
||||
};
|
||||
|
||||
if (shouldRenderIcon) {
|
||||
return renderIcon();
|
||||
}
|
||||
|
||||
const classes = {
|
||||
...classesBase,
|
||||
thumbnails: true,
|
||||
};
|
||||
|
||||
return renderTask(
|
||||
this._embedThumbnailTask,
|
||||
(embeddedThumbnail: string | null) =>
|
||||
@@ -98,9 +105,7 @@ export class AdvancedCameraCardNextPreviousControl extends LitElement {
|
||||
{
|
||||
inProgressFunc: () => html`<div class=${classMap(classes)}></div>`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
errorFunc: (_e: Error) => {
|
||||
this._thumbnailError = true;
|
||||
},
|
||||
errorFunc: (_ev: Error) => renderIcon(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,8 +42,7 @@ export class AdvancedCameraCardProgressIndicator extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html` <div class="message vertical">
|
||||
${this.animated
|
||||
? html`<ha-circular-progress indeterminate size="${this.size}">
|
||||
</ha-circular-progress>`
|
||||
? html`<ha-spinner indeterminate size="${this.size}"> </ha-spinner>`
|
||||
: html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:timer-sand' }}
|
||||
></advanced-camera-card-icon>`}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { contentsChanged } from '../utils/basic';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event';
|
||||
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||
import { grSelectElements } from '../scoped-elements/gr-select';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import '../scoped-elements/gr-select';
|
||||
|
||||
export interface SelectOption {
|
||||
|
||||
@@ -3,13 +3,13 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { actionHandler } from '../../action-handler-directive.js';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import submenuStyle from '../../scss/submenu.scss';
|
||||
import {
|
||||
hasAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../../utils/action.js';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import '../icon.js';
|
||||
import { SubmenuInteraction, SubmenuItem } from './types.js';
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@ import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
|
||||
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
|
||||
import { computeDomain } from '../../ha/compute-domain.js';
|
||||
import { getEntityStateTranslation } from '../../ha/entity-state-translation.js';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title.js';
|
||||
import { isHassDifferent } from '../../ha/is-hass-different.js';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||
import { Icon } from '../../types.js';
|
||||
import { createSelectOptionAction } from '../../utils/action.js';
|
||||
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
||||
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types.js';
|
||||
import '../icon.js';
|
||||
import './index.js';
|
||||
|
||||
|
||||
+13
-25
@@ -8,7 +8,7 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { ThumbnailsControlConfig } from '../config/schema/common/controls/thumbnails.js';
|
||||
import { MiniTimelineControlConfig } from '../config/schema/common/controls/timeline.js';
|
||||
@@ -17,8 +17,9 @@ import { HomeAssistant } from '../ha/types.js';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event.js';
|
||||
import { QueryClassifier } from '../view/query-classifier.js';
|
||||
import './surround-basic.js';
|
||||
import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||
import './thumbnail-carousel';
|
||||
|
||||
@customElement('advanced-camera-card-surround')
|
||||
export class AdvancedCameraCardSurround extends LitElement {
|
||||
@@ -37,6 +38,9 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@@ -92,8 +96,10 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
.getAllDependentCameras(view.camera, capabilitySearch);
|
||||
}
|
||||
}
|
||||
if (view.isViewerView()) {
|
||||
return view.query?.getQueryCameraIDs() ?? null;
|
||||
|
||||
const queries = view.query;
|
||||
if (view.isViewerView() && QueryClassifier.isMediaQuery(queries)) {
|
||||
return queries.getQueryCameraIDs() ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -118,9 +124,7 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
};
|
||||
|
||||
return html` <advanced-camera-card-surround-basic
|
||||
@advanced-camera-card:thumbnails:open=${(ev: CustomEvent) =>
|
||||
changeDrawer(ev, 'open')}
|
||||
@advanced-camera-card:thumbnails:close=${(ev: CustomEvent) =>
|
||||
@advanced-camera-card:thumbnails-carousel:media-select=${(ev: CustomEvent) =>
|
||||
changeDrawer(ev, 'close')}
|
||||
>
|
||||
${this.thumbnailConfig && this.thumbnailConfig.mode !== 'none'
|
||||
@@ -129,27 +133,10 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.config=${this.thumbnailConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.fadeThumbnails=${view.isViewerView()}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.selected=${view.queryResults?.getSelectedIndex() ?? undefined}
|
||||
@advanced-camera-card:thumbnail-carousel:tap=${(
|
||||
ev: CustomEvent<ThumbnailCarouselTap>,
|
||||
) => {
|
||||
const media = ev.detail.queryResults.getSelectedResult();
|
||||
if (media) {
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
params: {
|
||||
view: 'media',
|
||||
queryResults: ev.detail.queryResults,
|
||||
...(media.getCameraID() && { camera: media.getCameraID() }),
|
||||
},
|
||||
modifiers: [
|
||||
new RemoveContextViewModifier(['timeline', 'mediaViewer']),
|
||||
],
|
||||
});
|
||||
changeDrawer(ev, 'close');
|
||||
}
|
||||
}}
|
||||
>
|
||||
</advanced-camera-card-thumbnail-carousel>`
|
||||
: ''}
|
||||
@@ -168,6 +155,7 @@ export class AdvancedCameraCardSurround extends LitElement {
|
||||
.timelineConfig=${this.timelineConfig}
|
||||
.thumbnailConfig=${this.thumbnailConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</advanced-camera-card-timeline-core>`
|
||||
|
||||
@@ -9,19 +9,27 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import {
|
||||
getUpFolderMediaItem,
|
||||
upFolderClickHandler,
|
||||
} from '../components-lib/folder/up-folder.js';
|
||||
import { ThumbnailsControlConfig } from '../config/schema/common/controls/thumbnails.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event.js';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { ViewItemClassifier } from '../view/item-classifier.js';
|
||||
import { ViewItem, ViewMedia } from '../view/item.js';
|
||||
import { QueryClassifier } from '../view/query-classifier.js';
|
||||
import './carousel.js';
|
||||
import './thumbnail.js';
|
||||
import './thumbnail/thumbnail.js';
|
||||
|
||||
export interface ThumbnailCarouselTap {
|
||||
queryResults: MediaQueriesResults;
|
||||
export interface ThumbnailMediaSelect {
|
||||
media: ViewMedia;
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-carousel')
|
||||
@@ -35,13 +43,16 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: ThumbnailsControlConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public fadeThumbnails = false;
|
||||
|
||||
protected _thumbnailSlides: TemplateResult[] = [];
|
||||
protected _thumbnails: TemplateResult[] = [];
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('config')) {
|
||||
@@ -66,7 +77,7 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
|
||||
'viewManagerEpoch',
|
||||
] as const;
|
||||
if (renderProperties.some((prop) => changedProps.has(prop))) {
|
||||
this._thumbnailSlides = this._renderSlides();
|
||||
this._thumbnails = this._renderThumbnails();
|
||||
}
|
||||
|
||||
if (changedProps.has('viewManagerEpoch')) {
|
||||
@@ -83,49 +94,114 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
protected _renderSlides(): TemplateResult[] {
|
||||
const slides: TemplateResult[] = [];
|
||||
protected _itemClickCallback(item: ViewItem, ev: Event): void {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const query = view?.query;
|
||||
const results = view?.queryResults;
|
||||
|
||||
if (!view || !query || !results) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewItemClassifier.isMedia(item)) {
|
||||
const newResults = results
|
||||
.clone()
|
||||
.selectResultIfFound((result) => result === item);
|
||||
const cameraID = item.getCameraID();
|
||||
|
||||
fireAdvancedCameraCardEvent<ThumbnailMediaSelect>(
|
||||
this,
|
||||
'thumbnails-carousel:media-select',
|
||||
{ media: item },
|
||||
);
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
params: {
|
||||
view: 'media',
|
||||
queryResults: newResults,
|
||||
...(cameraID && { camera: cameraID }),
|
||||
},
|
||||
modifiers: [new RemoveContextViewModifier(['timeline', 'mediaViewer'])],
|
||||
});
|
||||
} else if (
|
||||
QueryClassifier.isFolderQuery(query) &&
|
||||
ViewItemClassifier.isFolder(item)
|
||||
) {
|
||||
const id = item.getID();
|
||||
const rawQuery = query.getQuery();
|
||||
if (!id || !rawQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewManagerEpoch?.manager.setViewByParametersWithExistingQuery({
|
||||
params: {
|
||||
query: query.clone().setQuery({
|
||||
folder: rawQuery.folder,
|
||||
path: [...(rawQuery.path ?? []), { id }],
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderThumbnail(
|
||||
item: ViewItem,
|
||||
selected: boolean,
|
||||
clickCallback: (item: ViewItem, ev: Event) => void,
|
||||
seekTarget?: Date,
|
||||
): TemplateResult {
|
||||
const classes = {
|
||||
embla__slide: true,
|
||||
'slide-selected': selected,
|
||||
};
|
||||
|
||||
return html` <advanced-camera-card-thumbnail
|
||||
class="${classMap(classes)}"
|
||||
.cameraManager=${this.cameraManager}
|
||||
.hass=${this.hass}
|
||||
.item=${item}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.seek=${seekTarget &&
|
||||
ViewItemClassifier.isMedia(item) &&
|
||||
item.includesTime(seekTarget)
|
||||
? seekTarget
|
||||
: undefined}
|
||||
?details=${!!this.config?.show_details}
|
||||
?show_favorite_control=${this.config?.show_favorite_control}
|
||||
?show_timeline_control=${this.config?.show_timeline_control}
|
||||
?show_download_control=${this.config?.show_download_control}
|
||||
@click=${(ev: Event) => clickCallback(item, ev)}
|
||||
>
|
||||
</advanced-camera-card-thumbnail>`;
|
||||
}
|
||||
|
||||
protected _renderThumbnails(): TemplateResult[] {
|
||||
const upThumbnail = getUpFolderMediaItem(this.viewManagerEpoch?.manager.getView());
|
||||
const thumbnails: TemplateResult[] = [
|
||||
...(upThumbnail
|
||||
? [
|
||||
this._renderThumbnail(upThumbnail, false, (item: ViewItem, ev: Event) =>
|
||||
upFolderClickHandler(item, ev, this.viewManagerEpoch),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const seekTarget = view?.context?.mediaViewer?.seek;
|
||||
const selectedIndex = this._getSelectedSlide();
|
||||
|
||||
for (const media of view?.queryResults?.getResults() ?? []) {
|
||||
const index = slides.length;
|
||||
const classes = {
|
||||
embla__slide: true,
|
||||
'slide-selected': selectedIndex === index,
|
||||
};
|
||||
|
||||
slides.push(
|
||||
html` <advanced-camera-card-thumbnail
|
||||
class="${classMap(classes)}"
|
||||
.cameraManager=${this.cameraManager}
|
||||
.hass=${this.hass}
|
||||
.media=${media}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.seek=${seekTarget && media.includesTime(seekTarget) ? seekTarget : undefined}
|
||||
?details=${!!this.config?.show_details}
|
||||
?show_favorite_control=${this.config?.show_favorite_control}
|
||||
?show_timeline_control=${this.config?.show_timeline_control}
|
||||
?show_download_control=${this.config?.show_download_control}
|
||||
@click=${(ev: Event) => {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
if (view && view.queryResults) {
|
||||
fireAdvancedCameraCardEvent<ThumbnailCarouselTap>(
|
||||
this,
|
||||
'thumbnail-carousel:tap',
|
||||
{
|
||||
queryResults: view.queryResults.clone().selectIndex(index),
|
||||
},
|
||||
);
|
||||
}
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
}}
|
||||
>
|
||||
</advanced-camera-card-thumbnail>`,
|
||||
for (const item of view?.queryResults?.getResults() ?? []) {
|
||||
thumbnails.push(
|
||||
this._renderThumbnail(
|
||||
item,
|
||||
selectedIndex === thumbnails.length,
|
||||
(item: ViewItem, ev: Event) => this._itemClickCallback(item, ev),
|
||||
view?.context?.mediaViewer?.seek,
|
||||
),
|
||||
);
|
||||
}
|
||||
return slides;
|
||||
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
protected _getDirection(): CarouselDirection | null {
|
||||
@@ -139,7 +215,7 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const direction = this._getDirection();
|
||||
if (!this._thumbnailSlides.length || !this.config || !direction) {
|
||||
if (!this._thumbnails.length || !this.config || !direction) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,7 +224,7 @@ export class AdvancedCameraCardThumbnailCarousel extends LitElement {
|
||||
.selected=${this._getSelectedSlide() ?? 0}
|
||||
.dragFree=${true}
|
||||
>
|
||||
${this._thumbnailSlides}
|
||||
${this._thumbnails}
|
||||
</advanced-camera-card-carousel>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,553 +0,0 @@
|
||||
import { Task, TaskStatus } from '@lit-labs/task';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
CSSResult,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { CameraManagerCameraMetadata } from '../camera-manager/types.js';
|
||||
import { RemoveContextViewModifier } from '../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { dispatchAdvancedCameraCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import thumbnailDetailsStyle from '../scss/thumbnail-details.scss';
|
||||
import thumbnailFeatureTextStyle from '../scss/thumbnail-feature-text.scss';
|
||||
import thumbnailFeatureThumbnailStyle from '../scss/thumbnail-feature-thumbnail.scss';
|
||||
import thumbnailStyle from '../scss/thumbnail.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||
import {
|
||||
errorToConsole,
|
||||
formatDateAndTime,
|
||||
getDurationString,
|
||||
prettifyTitle,
|
||||
} from '../utils/basic.js';
|
||||
import { downloadMedia } from '../utils/download.js';
|
||||
import { renderTask } from '../utils/task.js';
|
||||
import { createFetchThumbnailTask, FetchThumbnailTaskArgs } from '../utils/thumbnail.js';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier.js';
|
||||
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
|
||||
|
||||
// The minimum width of a thumbnail with details enabled.
|
||||
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-feature-thumbnail')
|
||||
export class AdvancedCameraCardThumbnailFeatureThumbnail extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public thumbnail?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@state()
|
||||
protected _thumbnailError = false;
|
||||
|
||||
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
||||
|
||||
// Only load thumbnails on view in case there is a very large number of them.
|
||||
protected _intersectionObserver: IntersectionObserver;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._intersectionObserver = new IntersectionObserver(
|
||||
this._intersectionHandler.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component connected callback.
|
||||
*/
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component disconnected callback.
|
||||
*/
|
||||
disconnectedCallback(): void {
|
||||
this._intersectionObserver.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('thumbnail')) {
|
||||
this._embedThumbnailTask = createFetchThumbnailTask(
|
||||
this,
|
||||
() => this.hass,
|
||||
() => this.thumbnail,
|
||||
false,
|
||||
);
|
||||
// Reset the observer so the initial intersection handler call will set
|
||||
// the visibility correctly.
|
||||
this._intersectionObserver.unobserve(this);
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the live view intersects with the viewport.
|
||||
* @param entries The IntersectionObserverEntry entries (should be only 1).
|
||||
*/
|
||||
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||
if (
|
||||
this._embedThumbnailTask?.status === TaskStatus.INITIAL &&
|
||||
entries.some((entry) => entry.isIntersecting)
|
||||
) {
|
||||
this._embedThumbnailTask?.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const imageOff = html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:image-off' }}
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
></advanced-camera-card-icon> `;
|
||||
|
||||
if (!this._embedThumbnailTask || this._thumbnailError) {
|
||||
return imageOff;
|
||||
}
|
||||
|
||||
return html`${this.thumbnail
|
||||
? renderTask(
|
||||
this._embedThumbnailTask,
|
||||
(embeddedThumbnail: string | null) =>
|
||||
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
||||
{
|
||||
inProgressFunc: () => imageOff,
|
||||
errorFunc: () => {
|
||||
this._thumbnailError = true;
|
||||
},
|
||||
},
|
||||
)
|
||||
: imageOff} `;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureThumbnailStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-feature-text')
|
||||
export class AdvancedCameraCardThumbnailFeatureText extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public date?: Date;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraMetadata?: CameraManagerCameraMetadata;
|
||||
|
||||
@property({ attribute: false })
|
||||
public showCameraTitle?: boolean;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.date) {
|
||||
return;
|
||||
}
|
||||
return html`
|
||||
${this.cameraMetadata?.engineIcon
|
||||
? html`<advanced-camera-card-icon
|
||||
class="background"
|
||||
.icon=${{ icon: this.cameraMetadata.engineIcon }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
<div class="content">
|
||||
<div class="title">${format(this.date, 'HH:mm')}</div>
|
||||
<div class="subtitle">${format(this.date, 'MMM do')}</div>
|
||||
${this.showCameraTitle && this.cameraMetadata?.title
|
||||
? html`<div class="camera">${this.cameraMetadata.title}</div>`
|
||||
: html``}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureTextStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-details-event')
|
||||
export class AdvancedCameraCardThumbnailDetailsEvent extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public media?: EventViewMedia;
|
||||
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraTitle?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.media) {
|
||||
return;
|
||||
}
|
||||
const rawScore = this.media.getScore();
|
||||
const score = rawScore ? (rawScore * 100).toFixed(2) + '%' : null;
|
||||
const rawStartTime = this.media.getStartTime();
|
||||
const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
|
||||
|
||||
const rawEndTime = this.media.getEndTime();
|
||||
const duration =
|
||||
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
|
||||
const inProgress = this.media.inProgress() ? localize('event.in_progress') : null;
|
||||
|
||||
const what = prettifyTitle(this.media.getWhat()?.join(', ')) ?? null;
|
||||
const where = prettifyTitle(this.media.getWhere()?.join(', ')) ?? null;
|
||||
const tags = prettifyTitle(this.media.getTags()?.join(', ')) ?? null;
|
||||
const whatWithTags =
|
||||
what || tags ? (what ?? '') + (what && tags ? ': ' : '') + (tags ?? '') : null;
|
||||
|
||||
const seek = this.seek ? format(this.seek, 'HH:mm:ss') : null;
|
||||
|
||||
return html`
|
||||
${whatWithTags
|
||||
? html` <div class="title">
|
||||
<span title=${whatWithTags}>${whatWithTags}</span>
|
||||
${score ? html`<span title="${score}">${score}</span>` : ''}
|
||||
</div>`
|
||||
: ``}
|
||||
<div class="details">
|
||||
${startTime
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.start')}
|
||||
.icon=${{ icon: 'mdi:calendar-clock-outline' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${startTime}">${startTime}</span>
|
||||
</div>
|
||||
${duration || inProgress
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.duration')}
|
||||
.icon=${{ icon: 'mdi:clock-outline' }}
|
||||
></advanced-camera-card-icon>
|
||||
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
|
||||
${inProgress
|
||||
? html`<span title="${inProgress}">${inProgress}</span>`
|
||||
: ''}
|
||||
</div>`
|
||||
: ''}`
|
||||
: ''}
|
||||
${this.cameraTitle
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.camera')}
|
||||
.icon=${{ icon: 'mdi:cctv' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${this.cameraTitle}">${this.cameraTitle}</span>
|
||||
</div>`
|
||||
: ''}
|
||||
${where
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.where')}
|
||||
.icon=${{ icon: 'mdi:map-marker-outline' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${where}">${where}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${tags
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.tag')}
|
||||
.icon=${{ icon: 'mdi:tag' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${tags}">${tags}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${seek
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.seek')}
|
||||
.icon=${{ icon: 'mdi:clock-fast' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${seek}">${seek}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailDetailsStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-details-recording')
|
||||
export class AdvancedCameraCardThumbnailDetailsRecording extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public media?: RecordingViewMedia;
|
||||
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraTitle?: string;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.media) {
|
||||
return;
|
||||
}
|
||||
const rawStartTime = this.media.getStartTime();
|
||||
const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
|
||||
|
||||
const rawEndTime = this.media.getEndTime();
|
||||
const duration =
|
||||
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
|
||||
const inProgress = this.media.inProgress()
|
||||
? localize('recording.in_progress')
|
||||
: null;
|
||||
|
||||
const seek = this.seek ? format(this.seek, 'HH:mm:ss') : null;
|
||||
|
||||
const eventCount = this.media.getEventCount();
|
||||
return html`
|
||||
${this.cameraTitle
|
||||
? html` <div class="title">
|
||||
<span title="${this.cameraTitle}">${this.cameraTitle}</span>
|
||||
</div>`
|
||||
: ``}
|
||||
<div class="details">
|
||||
${startTime
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('recording.start')}
|
||||
.icon=${{ icon: 'mdi:calendar-clock-outline' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${startTime}">${startTime}</span>
|
||||
</div>
|
||||
${duration || inProgress
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('recording.duration')}
|
||||
.icon=${{ icon: 'mdi:clock-outline' }}
|
||||
></advanced-camera-card-icon>
|
||||
${duration ? html`<span title="${duration}">${duration}</span>` : ''}
|
||||
${inProgress
|
||||
? html`<span title="${inProgress}">${inProgress}</span>`
|
||||
: ''}
|
||||
</div>`
|
||||
: ''}`
|
||||
: ''}
|
||||
${seek
|
||||
? html` <div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('event.seek')}
|
||||
.icon=${{ icon: 'mdi:clock-fast' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${seek}">${seek}</span>
|
||||
</div>`
|
||||
: html``}
|
||||
${eventCount !== null
|
||||
? html`<div>
|
||||
<advanced-camera-card-icon
|
||||
title=${localize('recording.events')}
|
||||
.icon=${{ icon: 'mdi:shield-alert' }}
|
||||
></advanced-camera-card-icon>
|
||||
<span title="${eventCount}">${eventCount}</span>
|
||||
</div>`
|
||||
: ``}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailDetailsStyle);
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail')
|
||||
export class AdvancedCameraCardThumbnail extends LitElement {
|
||||
// Performance: During timeline scrubbing, hass may be updated continuously.
|
||||
// As it is not needed for the thumbnail rendering itself, it does not trigger
|
||||
// a re-render. The HomeAssistant object may be required for thumbnail signing
|
||||
// (after initial signing the thumbnail is stored in a data URL, so the
|
||||
// signing will not expire).
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
// Performance: During timeline scrubbing, the view will be updated
|
||||
// continuously. As it is not needed for the thumbnail rendering itself, it
|
||||
// does not trigger a re-render.
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public media?: ViewMedia;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public details = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_favorite_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_timeline_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_download_control = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.media || !this.cameraManager || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thumbnail = this.media.getThumbnail();
|
||||
const title = this.media.getTitle() ?? '';
|
||||
|
||||
const starClasses = {
|
||||
star: true,
|
||||
starred: !!this.media?.isFavorite(),
|
||||
};
|
||||
|
||||
const shouldShowTimelineControl =
|
||||
this.show_timeline_control &&
|
||||
(!ViewMediaClassifier.isRecording(this.media) ||
|
||||
// Only show timeline control if the recording has a start & end time.
|
||||
(this.media.getStartTime() && this.media.getEndTime()));
|
||||
|
||||
const mediaCapabilities = this.cameraManager?.getMediaCapabilities(this.media);
|
||||
|
||||
const shouldShowFavoriteControl =
|
||||
this.show_favorite_control &&
|
||||
this.media &&
|
||||
this.hass &&
|
||||
mediaCapabilities?.canFavorite;
|
||||
|
||||
const shouldShowDownloadControl =
|
||||
this.show_download_control &&
|
||||
this.hass &&
|
||||
this.media.getID() &&
|
||||
mediaCapabilities?.canDownload;
|
||||
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(
|
||||
this.media.getCameraID(),
|
||||
);
|
||||
|
||||
return html`
|
||||
${ViewMediaClassifier.isEvent(this.media) && thumbnail
|
||||
? html`<advanced-camera-card-thumbnail-feature-thumbnail
|
||||
aria-label="${title ?? ''}"
|
||||
title=${title}
|
||||
.hass=${this.hass}
|
||||
.date=${this.media.getStartTime() ?? undefined}
|
||||
.thumbnail=${thumbnail ?? undefined}
|
||||
></advanced-camera-card-thumbnail-feature-thumbnail>`
|
||||
: ViewMediaClassifier.isEvent(this.media) ||
|
||||
ViewMediaClassifier.isRecording(this.media)
|
||||
? html`<advanced-camera-card-thumbnail-feature-text
|
||||
aria-label="${title ?? ''}"
|
||||
title="${title ?? ''}"
|
||||
.cameraMetadata=${cameraMetadata}
|
||||
.showCameraTitle=${!this.details}
|
||||
.date=${this.media.getStartTime() ?? undefined}
|
||||
></advanced-camera-card-thumbnail-feature-text>`
|
||||
: html``}
|
||||
${shouldShowFavoriteControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="${classMap(starClasses)}"
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
.icon=${{ icon: this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline' }}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.hass && this.media) {
|
||||
try {
|
||||
await this.cameraManager?.favoriteMedia(
|
||||
this.media,
|
||||
!this.media?.isFavorite(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
/></advanced-camera-card-icon>`
|
||||
: ``}
|
||||
${this.details && ViewMediaClassifier.isEvent(this.media)
|
||||
? html`<advanced-camera-card-thumbnail-details-event
|
||||
.media=${this.media ?? undefined}
|
||||
.cameraTitle=${cameraMetadata?.title}
|
||||
.seek=${this.seek}
|
||||
></advanced-camera-card-thumbnail-details-event>`
|
||||
: this.details && ViewMediaClassifier.isRecording(this.media)
|
||||
? html`<advanced-camera-card-thumbnail-details-recording
|
||||
.media=${this.media ?? undefined}
|
||||
.cameraTitle=${cameraMetadata?.title}
|
||||
.seek=${this.seek}
|
||||
></advanced-camera-card-thumbnail-details-recording>`
|
||||
: html``}
|
||||
${shouldShowTimelineControl
|
||||
? html`<advanced-camera-card-icon
|
||||
class="timeline"
|
||||
.icon=${{ icon: 'mdi:target' }}
|
||||
title=${localize('thumbnail.timeline')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (!this.viewManagerEpoch || !this.media) {
|
||||
return;
|
||||
}
|
||||
this.viewManagerEpoch.manager.setViewByParameters({
|
||||
params: {
|
||||
view: 'timeline',
|
||||
queryResults: this.viewManagerEpoch?.manager
|
||||
.getView()
|
||||
?.queryResults?.clone()
|
||||
.selectResultIfFound((media) => media === this.media),
|
||||
},
|
||||
modifiers: [new RemoveContextViewModifier(['timeline'])],
|
||||
});
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${shouldShowDownloadControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="download"
|
||||
.icon=${{ icon: 'mdi:download' }}
|
||||
title=${localize('thumbnail.download')}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.hass && this.cameraManager && this.media) {
|
||||
try {
|
||||
await downloadMedia(this.hass, this.cameraManager, this.media);
|
||||
} catch (error: unknown) {
|
||||
dispatchAdvancedCameraCardErrorEvent(this, error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element styles.
|
||||
*/
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-thumbnail': AdvancedCameraCardThumbnail;
|
||||
'advanced-camera-card-thumbnail-details-recording': AdvancedCameraCardThumbnailDetailsRecording;
|
||||
'advanced-camera-card-thumbnail-details-event': AdvancedCameraCardThumbnailDetailsEvent;
|
||||
'advanced-camera-card-thumbnail-feature-text': AdvancedCameraCardThumbnailFeatureText;
|
||||
'advanced-camera-card-thumbnail-feature-thumbnail': AdvancedCameraCardThumbnailFeatureThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { CameraManager } from '../../camera-manager/manager';
|
||||
import { ThumbnailDetailsController } from '../../components-lib/thumbnail/details-controller';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import thumbnailDetailsStyle from '../../scss/thumbnail-details.scss';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import '../icon';
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-details')
|
||||
export class AdvancedCameraCardThumbnailDetails extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public item?: ViewItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
private _controller = new ThumbnailDetailsController();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (['item', 'seek', 'cameraManager'].some((prop) => changedProperties.has(prop))) {
|
||||
this._controller.calculate(this.cameraManager, this.item, this.seek);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const heading = this._controller.getHeading();
|
||||
const details = this._controller.getDetails();
|
||||
|
||||
return html`<div class="details">
|
||||
${heading
|
||||
? html` <div class="title">
|
||||
<span title=${heading}>${heading}</span>
|
||||
</div>`
|
||||
: ``}
|
||||
${details
|
||||
? details.map(
|
||||
(detail) =>
|
||||
html`<div>
|
||||
${detail.icon
|
||||
? html` <advanced-camera-card-icon
|
||||
title=${detail.hint ?? ''}
|
||||
.icon=${detail.icon}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
<span>${detail.title}</span>
|
||||
</div>`,
|
||||
)
|
||||
: ''}
|
||||
</div> `;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailDetailsStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-thumbnail-details': AdvancedCameraCardThumbnailDetails;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../../../camera-manager/manager';
|
||||
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import thumbnailFeatureStyle from '../../../scss/thumbnail-feature.scss';
|
||||
import { ViewItem } from '../../../view/item';
|
||||
import '../../icon.js';
|
||||
import './thumbnail.js';
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-feature')
|
||||
export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public item?: ViewItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hasDetails?: boolean;
|
||||
|
||||
private _controller = new ThumbnailFeatureController();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
['item', 'hasDetails', 'cameraManager'].some((prop) => changedProperties.has(prop))
|
||||
) {
|
||||
this._controller.calculate(this.cameraManager, this.item, this.hasDetails);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const title = this._controller.getTitle();
|
||||
const subtitles = this._controller.getSubtitles();
|
||||
const iconClasses = classMap({
|
||||
background: title || subtitles.length,
|
||||
});
|
||||
|
||||
const thumbnailClass = this._controller.getThumbnailClass();
|
||||
const thumbnailClasses = classMap({
|
||||
...(thumbnailClass && { [thumbnailClass]: true }),
|
||||
});
|
||||
|
||||
return html`
|
||||
${this._controller.getThumbnail()
|
||||
? html` <advanced-camera-card-thumbnail-feature-thumbnail
|
||||
class="${thumbnailClasses}"
|
||||
.hass=${this.hass}
|
||||
.thumbnail=${this._controller.getThumbnail()}
|
||||
aria-label=${this._controller.getTitle() ?? ''}
|
||||
title=${this._controller.getTitle() ?? ''}
|
||||
></advanced-camera-card-thumbnail-feature-thumbnail>`
|
||||
: this._controller.getIcon()
|
||||
? html`<advanced-camera-card-icon
|
||||
class="${iconClasses}"
|
||||
.icon=${{ icon: this._controller.getIcon() }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${title || subtitles.length
|
||||
? html`
|
||||
${title ? html`<div class="title">${title}</div>` : ''}
|
||||
${subtitles.length
|
||||
? html`<div>
|
||||
${subtitles.map(
|
||||
(subtitle) => html`<div class="subtitle">${subtitle}</div>`,
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
`
|
||||
: html``}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-thumbnail-feature': AdvancedCameraCardThumbnailFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Task, TaskStatus } from '@lit-labs/task';
|
||||
import {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
html,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { localize } from '../../../localize/localize';
|
||||
import thumbnailFeatureThumbnailStyle from '../../../scss/thumbnail-feature-thumbnail.scss';
|
||||
import { renderTask } from '../../../utils/task';
|
||||
import {
|
||||
FetchThumbnailTaskArgs,
|
||||
createFetchThumbnailTask,
|
||||
} from '../../../utils/thumbnail';
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail-feature-thumbnail')
|
||||
export class AdvancedCameraCardThumbnailFeatureThumbnail extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public thumbnail?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
protected _embedThumbnailTask?: Task<FetchThumbnailTaskArgs, string | null>;
|
||||
|
||||
// Only load thumbnails on view in case there is a very large number of them.
|
||||
protected _intersectionObserver = new IntersectionObserver(
|
||||
this._intersectionHandler.bind(this),
|
||||
);
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._intersectionObserver.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('thumbnail')) {
|
||||
this._embedThumbnailTask = createFetchThumbnailTask(
|
||||
this,
|
||||
() => this.hass,
|
||||
() => this.thumbnail,
|
||||
false,
|
||||
);
|
||||
// Reset the observer so the initial intersection handler call will set
|
||||
// the visibility correctly.
|
||||
this._intersectionObserver.unobserve(this);
|
||||
this._intersectionObserver.observe(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected _intersectionHandler(entries: IntersectionObserverEntry[]): void {
|
||||
if (
|
||||
this._embedThumbnailTask?.status === TaskStatus.INITIAL &&
|
||||
entries.some((entry) => entry.isIntersecting)
|
||||
) {
|
||||
this._embedThumbnailTask?.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const imageOff = html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:image-off' }}
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
></advanced-camera-card-icon> `;
|
||||
|
||||
if (!this._embedThumbnailTask) {
|
||||
return imageOff;
|
||||
}
|
||||
|
||||
return html`${this.thumbnail
|
||||
? renderTask(
|
||||
this._embedThumbnailTask,
|
||||
(embeddedThumbnail: string | null) =>
|
||||
embeddedThumbnail ? html`<img src="${embeddedThumbnail}" />` : html``,
|
||||
{
|
||||
inProgressFunc: () =>
|
||||
html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:image-refresh' }}
|
||||
title=${localize('thumbnail.no_thumbnail')}
|
||||
></advanced-camera-card-icon> `,
|
||||
errorFunc: () => imageOff,
|
||||
},
|
||||
)
|
||||
: imageOff} `;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailFeatureThumbnailStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-thumbnail-feature-thumbnail': AdvancedCameraCardThumbnailFeatureThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { FoldersManager } from '../../card-controller/folders/manager.js';
|
||||
import { ViewItemManager } from '../../card-controller/view/item-manager.js';
|
||||
import { RemoveContextViewModifier } from '../../card-controller/view/modifiers/remove-context.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { dispatchAdvancedCameraCardErrorEvent } from '../../components-lib/message/dispatch.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import thumbnailStyle from '../../scss/thumbnail.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { errorToConsole } from '../../utils/basic.js';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { ViewItem } from '../../view/item.js';
|
||||
import './details.js';
|
||||
import './feature/feature.js';
|
||||
import './feature/thumbnail.js';
|
||||
|
||||
@customElement('advanced-camera-card-thumbnail')
|
||||
export class AdvancedCameraCardThumbnail extends LitElement {
|
||||
// Performance: During timeline scrubbing, hass may be updated continuously.
|
||||
// As it is not needed for the thumbnail rendering itself, it does not trigger
|
||||
// a re-render. The HomeAssistant object may be required for thumbnail signing
|
||||
// (after initial signing the thumbnail is stored in a data URL, so the
|
||||
// signing will not expire).
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
// Performance: During timeline scrubbing, the view will be updated
|
||||
// continuously. As it is not needed for the thumbnail rendering itself, it
|
||||
// does not trigger a re-render.
|
||||
public viewManagerEpoch?: ViewManagerEpoch;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public folderManager?: FoldersManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public item?: ViewItem;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public details = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_favorite_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_timeline_control = false;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public show_download_control = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
/**
|
||||
* Render the element.
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const starClasses = {
|
||||
star: true,
|
||||
starred: ViewItemClassifier.isMedia(this.item) && !!this.item?.isFavorite(),
|
||||
};
|
||||
|
||||
const shouldShowTimelineControl =
|
||||
this.show_timeline_control &&
|
||||
((ViewItemClassifier.isEvent(this.item) && this.item.getStartTime()) ||
|
||||
(ViewItemClassifier.isRecording(this.item) &&
|
||||
this.item.getStartTime() &&
|
||||
this.item.getEndTime()));
|
||||
|
||||
const mediaCapabilities = this.viewItemManager?.getCapabilities(this.item) ?? null;
|
||||
|
||||
const shouldShowFavoriteControl =
|
||||
this.show_favorite_control &&
|
||||
this.item &&
|
||||
this.hass &&
|
||||
mediaCapabilities?.canFavorite;
|
||||
|
||||
const shouldShowDownloadControl =
|
||||
this.show_download_control &&
|
||||
this.hass &&
|
||||
this.item.getID() &&
|
||||
mediaCapabilities?.canDownload;
|
||||
|
||||
return html`
|
||||
<advanced-camera-card-thumbnail-feature
|
||||
aria-label=${this.item.getTitle() ?? ''}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.hasDetails=${this.details}
|
||||
.hass=${this.hass}
|
||||
.item=${this.item}
|
||||
>
|
||||
</advanced-camera-card-thumbnail-feature>
|
||||
${shouldShowFavoriteControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="${classMap(starClasses)}"
|
||||
title=${localize('thumbnail.retain_indefinitely')}
|
||||
.icon=${{ icon: this.item.isFavorite() ? 'mdi:star' : 'mdi:star-outline' }}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.hass && this.item) {
|
||||
try {
|
||||
await this.viewItemManager?.favorite(
|
||||
this.item,
|
||||
!this.item.isFavorite(),
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
return;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
/></advanced-camera-card-icon>`
|
||||
: ``}
|
||||
${this.details
|
||||
? html`<advanced-camera-card-thumbnail-details
|
||||
.hass=${this.hass}
|
||||
.item=${this.item ?? undefined}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.seek=${this.seek}
|
||||
></advanced-camera-card-thumbnail-details>`
|
||||
: ''}
|
||||
${shouldShowTimelineControl
|
||||
? html`<advanced-camera-card-icon
|
||||
class="timeline"
|
||||
.icon=${{ icon: 'mdi:target' }}
|
||||
title=${localize('thumbnail.timeline')}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (!this.viewManagerEpoch || !this.item) {
|
||||
return;
|
||||
}
|
||||
this.viewManagerEpoch.manager.setViewByParameters({
|
||||
params: {
|
||||
view: 'timeline',
|
||||
queryResults: this.viewManagerEpoch?.manager
|
||||
.getView()
|
||||
?.queryResults?.clone()
|
||||
.selectResultIfFound((media) => media === this.item),
|
||||
},
|
||||
modifiers: [new RemoveContextViewModifier(['timeline'])],
|
||||
});
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
${shouldShowDownloadControl
|
||||
? html` <advanced-camera-card-icon
|
||||
class="download"
|
||||
.icon=${{ icon: 'mdi:download' }}
|
||||
title=${localize('thumbnail.download')}
|
||||
@click=${async (ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
if (this.hass && this.item) {
|
||||
try {
|
||||
this.viewItemManager?.download(this.item);
|
||||
} catch (error: unknown) {
|
||||
dispatchAdvancedCameraCardErrorEvent(this, error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
></advanced-camera-card-icon>`
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return unsafeCSS(thumbnailStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-thumbnail': AdvancedCameraCardThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { isEqual, throttle } from 'lodash-es';
|
||||
import { ViewContext } from 'view';
|
||||
import { DataSet } from 'vis-data/esnext';
|
||||
import type {
|
||||
@@ -31,6 +30,7 @@ import { CameraManager } from '../camera-manager/manager';
|
||||
import { rangesOverlap } from '../camera-manager/range';
|
||||
import { MediaQuery } from '../camera-manager/types';
|
||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/utils/range-to-cache-friendly';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager';
|
||||
import { MergeContextViewModifier } from '../card-controller/view/modifiers/merge-context';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||
import {
|
||||
@@ -56,25 +56,18 @@ import {
|
||||
isTruthy,
|
||||
setOrRemoveAttribute,
|
||||
} from '../utils/basic';
|
||||
import { findBestMediaIndex } from '../utils/find-best-media-index';
|
||||
import { findBestMediaTimeIndex } from '../utils/find-best-media-time-index';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
MediaQueries,
|
||||
RecordingMediaQueries,
|
||||
} from '../view/media-queries';
|
||||
import {
|
||||
MediaQueriesClassifier,
|
||||
MediaQueriesType,
|
||||
} from '../view/media-queries-classifier';
|
||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||
import { ViewMedia } from '../view/item';
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { EventMediaQuery, MediaQueries, RecordingMediaQuery } from '../view/query';
|
||||
import { QueryClassifier, QueryType } from '../view/query-classifier';
|
||||
import { QueryResults } from '../view/query-results';
|
||||
import { mergeViewContext } from '../view/view';
|
||||
import './date-picker.js';
|
||||
import { AdvancedCameraCardDatePicker, DatePickerEvent } from './date-picker.js';
|
||||
import './icon';
|
||||
import './thumbnail.js';
|
||||
import './thumbnail/thumbnail.js';
|
||||
|
||||
interface AdvancedCameraCardGroupData {
|
||||
id: string;
|
||||
@@ -112,6 +105,7 @@ interface ThumbnailDataRequest {
|
||||
cameraConfig?: CameraConfig;
|
||||
media?: ViewMedia;
|
||||
viewManagerEpoch?: ViewManagerEpoch;
|
||||
viewItemManager?: ViewItemManager;
|
||||
}
|
||||
|
||||
class ThumbnailDataRequestEvent extends CustomEvent<ThumbnailDataRequest> {}
|
||||
@@ -119,7 +113,7 @@ class ThumbnailDataRequestEvent extends CustomEvent<ThumbnailDataRequest> {}
|
||||
const TIMELINE_TARGET_BAR_ID = 'target_bar';
|
||||
|
||||
/**
|
||||
* A simgple thumbnail wrapper class for use in the timeline where Lit data
|
||||
* A simple thumbnail wrapper class for use in the timeline where Lit data
|
||||
* bindings are not available.
|
||||
*/
|
||||
@customElement('advanced-camera-card-timeline-thumbnail')
|
||||
@@ -166,6 +160,7 @@ export class AdvancedCameraCardTimelineThumbnail extends LitElement {
|
||||
!dataRequest.hass ||
|
||||
!dataRequest.cameraManager ||
|
||||
!dataRequest.cameraConfig ||
|
||||
!dataRequest.viewItemManager ||
|
||||
!dataRequest.media ||
|
||||
!dataRequest.viewManagerEpoch
|
||||
) {
|
||||
@@ -175,7 +170,8 @@ export class AdvancedCameraCardTimelineThumbnail extends LitElement {
|
||||
return html` <advanced-camera-card-thumbnail
|
||||
.hass=${dataRequest.hass}
|
||||
.cameraManager=${dataRequest.cameraManager}
|
||||
.media=${dataRequest.media}
|
||||
.viewItemManager=${dataRequest.viewItemManager}
|
||||
.item=${dataRequest.media}
|
||||
.viewManagerEpoch=${dataRequest.viewManagerEpoch}
|
||||
?details=${this.details}
|
||||
>
|
||||
@@ -210,6 +206,9 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@@ -275,6 +274,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
request.detail.hass = this.hass;
|
||||
request.detail.cameraConfig = cameraConfig;
|
||||
request.detail.cameraManager = this.cameraManager;
|
||||
request.detail.viewItemManager = this.viewItemManager;
|
||||
request.detail.media = media;
|
||||
request.detail.viewManagerEpoch = this.viewManagerEpoch;
|
||||
}
|
||||
@@ -477,13 +477,13 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
}
|
||||
|
||||
const canSeek = this._shouldSupportSeeking();
|
||||
let newResults: MediaQueriesResults | null = null;
|
||||
let newResults: QueryResults | null = null;
|
||||
|
||||
if (panMode === 'seek') {
|
||||
newResults = results
|
||||
.clone()
|
||||
.selectBestResult(
|
||||
(mediaArray) => findBestMediaIndex(mediaArray, targetTime, view?.camera),
|
||||
(mediaArray) => findBestMediaTimeIndex(mediaArray, targetTime, view?.camera),
|
||||
{
|
||||
allCameras: true,
|
||||
main: true,
|
||||
@@ -492,9 +492,12 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
} else if (panMode === 'seek-in-camera') {
|
||||
newResults = results
|
||||
.clone()
|
||||
.selectBestResult((mediaArray) => findBestMediaIndex(mediaArray, targetTime), {
|
||||
cameraID: view.camera,
|
||||
})
|
||||
.selectBestResult(
|
||||
(mediaArray) => findBestMediaTimeIndex(mediaArray, targetTime),
|
||||
{
|
||||
cameraID: view.camera,
|
||||
},
|
||||
)
|
||||
.promoteCameraSelectionToMainSelection(view.camera);
|
||||
} else if (panMode === 'seek-in-media') {
|
||||
newResults = results;
|
||||
@@ -506,7 +509,10 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
: 'media'
|
||||
: view.view;
|
||||
|
||||
const selectedCamera = newResults?.getSelectedResult()?.getCameraID();
|
||||
const selectedItem = newResults?.getSelectedResult();
|
||||
const selectedCamera = ViewItemClassifier.isMedia(selectedItem)
|
||||
? selectedItem.getCameraID()
|
||||
: null;
|
||||
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
params: {
|
||||
@@ -675,7 +681,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
|
||||
await this._timelineSource?.refresh(this._getPrefetchWindow(properties));
|
||||
|
||||
const queryType = MediaQueriesClassifier.getQueriesType(view.query);
|
||||
const queryType = QueryClassifier.getQueryType(view.query);
|
||||
if (!queryType) {
|
||||
return;
|
||||
}
|
||||
@@ -702,7 +708,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
}
|
||||
|
||||
protected _createMediaQueries(
|
||||
type: MediaQueriesType,
|
||||
type: QueryType,
|
||||
options?: {
|
||||
window?: TimelineWindow;
|
||||
},
|
||||
@@ -717,11 +723,11 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
|
||||
if (type === 'event') {
|
||||
const queries = this._timelineSource.getTimelineEventQueries(cacheFriendlyWindow);
|
||||
return queries ? new EventMediaQueries(queries) : null;
|
||||
return queries ? new EventMediaQuery(queries) : null;
|
||||
} else if (type === 'recording') {
|
||||
const queries =
|
||||
this._timelineSource.getTimelineRecordingQueries(cacheFriendlyWindow);
|
||||
return queries ? new RecordingMediaQueries(queries) : null;
|
||||
return queries ? new RecordingMediaQuery(queries) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -877,8 +883,8 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
!selectedIDs.includes(second.id) &&
|
||||
!!firstMedia &&
|
||||
!!secondMedia &&
|
||||
ViewMediaClassifier.isEvent(firstMedia) &&
|
||||
ViewMediaClassifier.isEvent(secondMedia) &&
|
||||
ViewItemClassifier.isEvent(firstMedia) &&
|
||||
ViewItemClassifier.isEvent(secondMedia) &&
|
||||
firstMedia.isGroupableWith(secondMedia)
|
||||
);
|
||||
},
|
||||
@@ -938,7 +944,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
...(view.isGrid() && { allCameras: true }),
|
||||
}) ?? []
|
||||
)
|
||||
.filter((media) => ViewMediaClassifier.isEvent(media))
|
||||
.filter((media) => ViewItemClassifier.isEvent(media))
|
||||
.map((media) => media.getID())
|
||||
.filter(isTruthy);
|
||||
}
|
||||
@@ -961,10 +967,11 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
// perfectly center on the media.
|
||||
|
||||
let desiredWindow = timelineWindow;
|
||||
const media = view.queryResults?.getSelectedResult();
|
||||
const item = view.queryResults?.getSelectedResult();
|
||||
const media = item && ViewItemClassifier.isMedia(item) ? item : null;
|
||||
const mediaStartTime = media?.getStartTime() ?? null;
|
||||
const mediaEndTime = media?.getEndTime() ?? null;
|
||||
const mediaIsEvent = media ? ViewMediaClassifier.isEvent(media) : false;
|
||||
const mediaIsEvent = media ? ViewItemClassifier.isEvent(media) : false;
|
||||
|
||||
const mediaWindow: TimelineWindow | null =
|
||||
media && mediaStartTime
|
||||
@@ -1044,7 +1051,7 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
// Also don't generate thumbnails in mini-timelines (they will already have
|
||||
// been generated).
|
||||
|
||||
const queryType = MediaQueriesClassifier.getQueriesType(view.query);
|
||||
const queryType = QueryClassifier.getQueryType(view.query);
|
||||
if (!queryType) {
|
||||
return;
|
||||
}
|
||||
@@ -1080,15 +1087,19 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
|
||||
protected _alreadyHasAcceptableMediaQuery(freshMediaQuery: MediaQueries): boolean {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const query = view?.query;
|
||||
|
||||
const currentQueries = view?.query?.getQueries();
|
||||
if (!this.cameraManager || !query || !QueryClassifier.isMediaQuery(query)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentQueries = query?.getQuery();
|
||||
const currentResultTimestamp = view?.queryResults?.getResultsTimestamp();
|
||||
|
||||
return (
|
||||
!!this.cameraManager &&
|
||||
!!currentQueries &&
|
||||
!!currentResultTimestamp &&
|
||||
!!view?.query?.isSupersetOf(freshMediaQuery) &&
|
||||
!!query?.isSupersetOf(freshMediaQuery) &&
|
||||
this.cameraManager.areMediaQueriesResultsFresh<MediaQuery>(
|
||||
currentQueries,
|
||||
currentResultTimestamp,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||
import { TimelineConfig } from '../config/schema/timeline';
|
||||
import { CardWideConfig } from '../config/schema/types';
|
||||
@@ -23,6 +24,9 @@ export class AdvancedCameraCardTimeline extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@@ -38,6 +42,7 @@ export class AdvancedCameraCardTimeline extends LitElement {
|
||||
.timelineConfig=${this.timelineConfig}
|
||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.cameraIDs=${this.cameraManager?.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
})}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MediaActionsController } from '../../components-lib/media-actions-contr
|
||||
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
@@ -26,9 +27,9 @@ import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js';
|
||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||
import { ResolvedMediaCache } from '../../utils/ha/resolved-media.js';
|
||||
import { getTextDirection } from '../../utils/text-direction.js';
|
||||
import { ViewMedia } from '../../view/media.js';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { ViewMedia } from '../../view/item.js';
|
||||
import '../carousel';
|
||||
import type { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
@@ -80,33 +81,13 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
public showControls = true;
|
||||
|
||||
@state()
|
||||
protected _selected = 0;
|
||||
protected _selected: number | null = null;
|
||||
|
||||
protected _media: ViewMedia[] | null = null;
|
||||
protected _mediaActionsController = new MediaActionsController();
|
||||
protected _loadedMediaPlayerController: MediaPlayerController | null = null;
|
||||
protected _refCarousel: Ref<HTMLElement> = createRef();
|
||||
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('viewManagerEpoch')) {
|
||||
// Seek into the video if the seek time has changed (this is also called
|
||||
// on media load, since the media may or may not have been loaded at
|
||||
// this point).
|
||||
if (
|
||||
this.viewManagerEpoch?.manager.getView()?.context?.mediaViewer !==
|
||||
this.viewManagerEpoch?.oldView?.context?.mediaViewer
|
||||
) {
|
||||
this._seekHandler();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._refCarousel.value) {
|
||||
this._mediaActionsController.setRoot(this._refCarousel.value);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -145,7 +126,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
*/
|
||||
protected _getMediaNeighbors(): MediaNeighbors | null {
|
||||
const mediaCount = this._media?.length ?? 0;
|
||||
if (!this._media) {
|
||||
if (!this._media || this._selected === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -180,16 +161,20 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
// index).
|
||||
return;
|
||||
}
|
||||
|
||||
const newResults = view?.queryResults
|
||||
?.clone()
|
||||
.selectIndex(index, this.viewFilterCameraID);
|
||||
.selectResultIfFound((item) => item === this._media?.[index], {
|
||||
main: true,
|
||||
cameraID: this.viewFilterCameraID,
|
||||
});
|
||||
if (!newResults) {
|
||||
return;
|
||||
}
|
||||
const cameraID = newResults
|
||||
.getSelectedResult(this.viewFilterCameraID)
|
||||
?.getCameraID();
|
||||
|
||||
const selectedItem = newResults.getSelectedResult(this.viewFilterCameraID);
|
||||
const cameraID = ViewItemClassifier.isMedia(selectedItem)
|
||||
? selectedItem.getCameraID()
|
||||
: null;
|
||||
|
||||
this.viewManagerEpoch?.manager.setViewByParameters({
|
||||
params: {
|
||||
@@ -243,33 +228,43 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
}
|
||||
|
||||
if (changedProps.has('viewManagerEpoch')) {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const newMedia = view?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
|
||||
const newSelected =
|
||||
view?.queryResults?.getSelectedIndex(this.viewFilterCameraID) ?? 0;
|
||||
const newSeek = view?.context?.mediaViewer?.seek;
|
||||
const newView = this.viewManagerEpoch?.manager.getView();
|
||||
|
||||
if (newMedia !== this._media || newSelected !== this._selected || !newSeek) {
|
||||
if (!newView?.context?.mediaViewer?.seek) {
|
||||
setOrRemoveAttribute(this, false, 'unseekable');
|
||||
this._media = newMedia;
|
||||
this._selected = newSelected;
|
||||
}
|
||||
|
||||
if (!newMedia?.length) {
|
||||
// No media will be rendered.
|
||||
this._mediaActionsController.unsetTarget();
|
||||
} else {
|
||||
if (this.viewFilterCameraID) {
|
||||
this._mediaActionsController.setTarget(
|
||||
newSelected,
|
||||
// Camera in this carousel is only selected if the camera from the
|
||||
// view matches the filtered camera.
|
||||
view?.camera === this.viewFilterCameraID,
|
||||
);
|
||||
} else {
|
||||
// Carousel is not filtered, so the targeted camera is always selected.
|
||||
this._mediaActionsController.setTarget(newSelected, true);
|
||||
}
|
||||
const oldView = this.viewManagerEpoch?.oldView;
|
||||
const oldItems =
|
||||
oldView?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
|
||||
const newItems =
|
||||
newView?.queryResults?.getResults(this.viewFilterCameraID) ?? null;
|
||||
let resetMedia = false;
|
||||
if (!this._media || oldItems !== newItems) {
|
||||
this._media =
|
||||
newItems?.filter((item) => ViewItemClassifier.isMedia(item)) ?? null;
|
||||
resetMedia = true;
|
||||
}
|
||||
|
||||
const oldSelectedItem = oldView?.queryResults?.getSelectedResult(
|
||||
this.viewFilterCameraID,
|
||||
);
|
||||
const newSelectedItem = newView?.queryResults?.getSelectedResult(
|
||||
this.viewFilterCameraID,
|
||||
);
|
||||
|
||||
// _selected is an index, it needs to be updated if either the selected
|
||||
// item or the media changes.
|
||||
if (oldSelectedItem !== newSelectedItem || resetMedia) {
|
||||
const newSelected =
|
||||
this._media?.findIndex((item) => item === newSelectedItem) ?? null;
|
||||
|
||||
// If there's no selected item, just choose the last (most recent one) to
|
||||
// avoid rendering a blank. This could happen if the selected item was a
|
||||
// folder.
|
||||
this._selected =
|
||||
newSelected ??
|
||||
(this._media && this._media.length ? this._media.length - 1 : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,12 +322,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
// If there's no selected media, just choose the last (most recent one) to
|
||||
// avoid rendering a blank. This situation should not occur in practice, as
|
||||
// this view should not be called without a selected media.
|
||||
const selectedMedia = this._media[this._selected] ?? this._media[mediaCount - 1];
|
||||
|
||||
if (!this.hass || !this.cameraManager || !selectedMedia) {
|
||||
if (!this.hass || !this.cameraManager || this._selected === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -379,13 +369,61 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const rootChanged = this._refCarousel.value
|
||||
? this._mediaActionsController.setRoot(this._refCarousel.value)
|
||||
: false;
|
||||
|
||||
// If the view has changed, or if the media actions controller has just been
|
||||
// initialized, then call the necessary media action.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1626
|
||||
if (rootChanged || changedProperties.has('viewManagerEpoch')) {
|
||||
this._setMediaTarget();
|
||||
}
|
||||
|
||||
if (changedProperties.has('viewManagerEpoch')) {
|
||||
// Seek into the video if the seek time has changed (this is also called
|
||||
// on media load, since the media may or may not have been loaded at
|
||||
// this point).
|
||||
if (
|
||||
this.viewManagerEpoch?.manager.getView()?.context?.mediaViewer !==
|
||||
this.viewManagerEpoch?.oldView?.context?.mediaViewer
|
||||
) {
|
||||
this._seekHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected _setMediaTarget(): void {
|
||||
if (!this._media?.length || this._selected === null) {
|
||||
this._mediaActionsController.unsetTarget();
|
||||
} else {
|
||||
this._mediaActionsController.setTarget(
|
||||
this._selected,
|
||||
// Camera in this carousel is only selected if the camera from the view
|
||||
// matches the filtered camera.
|
||||
this.viewFilterCameraID
|
||||
? this.viewManagerEpoch?.manager.getView()?.camera === this.viewFilterCameraID
|
||||
: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a media show event when a slide is selected.
|
||||
*/
|
||||
protected async _seekHandler(): Promise<void> {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const seek = view?.context?.mediaViewer?.seek;
|
||||
if (!this.hass || !seek || !this._media || !this._loadedMediaPlayerController) {
|
||||
if (
|
||||
!this.hass ||
|
||||
!seek ||
|
||||
!this._media ||
|
||||
!this._loadedMediaPlayerController ||
|
||||
this._selected === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const selectedMedia = this._media[this._selected];
|
||||
|
||||
@@ -14,9 +14,9 @@ import { MediaGridSelected } from '../../components-lib/media-grid-controller.js
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||
import { ResolvedMediaCache } from '../../utils/ha/resolved-media.js';
|
||||
import './carousel';
|
||||
|
||||
@customElement('advanced-camera-card-viewer-grid')
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerStyle from '../../scss/viewer.scss';
|
||||
import { ResolvedMediaCache } from '../../utils/ha/resolved-media.js';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import './grid';
|
||||
|
||||
@@ -42,6 +50,18 @@ export class AdvancedCameraCardViewer extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@property({ attribute: 'empty', reflect: true, type: Boolean })
|
||||
public isEmpty = false;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has('viewManagerEpoch')) {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
this.isEmpty = !view?.queryResults
|
||||
?.getResults()
|
||||
?.filter((result) => ViewItemClassifier.isMedia(result)).length;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (
|
||||
!this.hass ||
|
||||
@@ -53,7 +73,7 @@ export class AdvancedCameraCardViewer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.viewManagerEpoch.manager.getView()?.queryResults?.hasResults()) {
|
||||
if (this.isEmpty) {
|
||||
// Directly render an error message (instead of dispatching it upwards)
|
||||
// to preserve the mini-timeline if the user pans into an area with no
|
||||
// media.
|
||||
|
||||
@@ -16,31 +16,24 @@ import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||
import {
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
ResolvedMedia,
|
||||
} from '../../types.js';
|
||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
||||
import {
|
||||
canonicalizeHAURL,
|
||||
homeAssistantSignPath,
|
||||
isHARelativeURL,
|
||||
} from '../../utils/ha/index.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media.js';
|
||||
import { canonicalizeHAURL } from '../../ha/canonical-url.js';
|
||||
import { isHARelativeURL } from '../../ha/is-ha-relative-url.js';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../ha/resolved-media.js';
|
||||
import { homeAssistantSignPath } from '../../ha/sign-path.js';
|
||||
import { HomeAssistant, ResolvedMedia } from '../../ha/types.js';
|
||||
import {
|
||||
addDynamicProxyURL,
|
||||
getWebProxiedURL,
|
||||
shouldUseWebProxy,
|
||||
} from '../../utils/ha/web-proxy.js';
|
||||
} from '../../ha/web-proxy.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../../types.js';
|
||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
||||
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { VideoContentType, ViewMedia } from '../../view/item.js';
|
||||
import { QueryClassifier } from '../../view/query-classifier.js';
|
||||
import '../image-player.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
import '../video-player.js';
|
||||
@@ -93,8 +86,8 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
!this.media ||
|
||||
// If this specific media item has no clip, then do nothing (even if all
|
||||
// the other media items do).
|
||||
!ViewMediaClassifier.isEvent(this.media) ||
|
||||
!MediaQueriesClassifier.areEventQueries(view.query)
|
||||
!ViewItemClassifier.isEvent(this.media) ||
|
||||
!QueryClassifier.isEventQuery(view.query)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -103,7 +96,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
const clipQuery = view.query.clone();
|
||||
clipQuery.convertToClipsQueries();
|
||||
|
||||
const queries = clipQuery.getQueries();
|
||||
const queries = clipQuery.getQuery();
|
||||
if (!queries) {
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +147,8 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
return;
|
||||
}
|
||||
|
||||
const camera = this.cameraManager?.getStore().getCamera(this.media.getCameraID());
|
||||
const cameraID = this.media.getCameraID();
|
||||
const camera = cameraID ? this.cameraManager?.getStore().getCamera(cameraID) : null;
|
||||
const proxyConfig = camera?.getProxyConfig();
|
||||
|
||||
if (proxyConfig && shouldUseWebProxy(this.hass, proxyConfig, 'media')) {
|
||||
@@ -224,7 +218,9 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
}
|
||||
const cameraID = this.media.getCameraID();
|
||||
const mediaID = this.media.getID() ?? undefined;
|
||||
const cameraConfig = this.cameraManager?.getStore().getCameraConfig(cameraID);
|
||||
const cameraConfig = cameraID
|
||||
? this.cameraManager?.getStore().getCameraConfig(cameraID) ?? null
|
||||
: null;
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
|
||||
return this.viewerConfig?.zoomable
|
||||
@@ -269,7 +265,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
// Note: crossorigin="anonymous" is required on <video> below in order to
|
||||
// allow screenshot of motionEye videos which currently go cross-origin.
|
||||
return this._useZoomIfRequired(html`
|
||||
${ViewMediaClassifier.isVideo(this.media)
|
||||
${ViewItemClassifier.isVideo(this.media)
|
||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||
? html`<advanced-camera-card-ha-hls-player
|
||||
${ref(this._refProvider)}
|
||||
|
||||
+24
-7
@@ -10,13 +10,14 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { MicrophoneState } from '../card-controller/types.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
import { AdvancedCameraCardConfig, CardWideConfig } from '../config/schema/types.js';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types.js';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device/index.js';
|
||||
import { ResolvedMediaCache } from '../ha/resolved-media.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import viewsStyle from '../scss/views.scss';
|
||||
import { DeviceRegistryManager } from '../utils/ha/registry/device/index.js';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||
import './surround.js';
|
||||
|
||||
// As a special case: The diagnostics view is not dynamically loaded in case
|
||||
@@ -34,6 +35,9 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public viewItemManager?: ViewItemManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public config?: AdvancedCameraCardConfig;
|
||||
|
||||
@@ -64,14 +68,16 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
if (view?.is('live') || this._shouldLivePreload()) {
|
||||
import('./live/index.js');
|
||||
}
|
||||
if (view?.isGalleryView()) {
|
||||
import('./gallery.js');
|
||||
if (view?.isMediaGalleryView() && !view.is('folder')) {
|
||||
import('./gallery/media-gallery.js');
|
||||
} else if (view?.isViewerView()) {
|
||||
import('./viewer/index.js');
|
||||
} else if (view?.is('image')) {
|
||||
import('./image.js');
|
||||
} else if (view?.is('timeline')) {
|
||||
import('./timeline.js');
|
||||
} else if (view?.is('folder')) {
|
||||
import('./gallery/folder-gallery.js');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +144,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
${!this.hide && view?.is('image') && cameraConfig
|
||||
@@ -150,15 +157,16 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
>
|
||||
</advanced-camera-card-image>`
|
||||
: ``}
|
||||
${!this.hide && view?.isGalleryView()
|
||||
? html` <advanced-camera-card-gallery
|
||||
${!this.hide && view?.isMediaGalleryView() && !view.is('folder')
|
||||
? html` <advanced-camera-card-media-gallery
|
||||
.hass=${this.hass}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.galleryConfig=${this.config.media_gallery}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</advanced-camera-card-gallery>`
|
||||
</advanced-camera-card-media-gallery>`
|
||||
: ``}
|
||||
${!this.hide && view?.isViewerView()
|
||||
? html`
|
||||
@@ -179,6 +187,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.timelineConfig=${this.config.timeline}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
</advanced-camera-card-timeline>`
|
||||
@@ -191,6 +200,14 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
>
|
||||
</advanced-camera-card-diagnostics>`
|
||||
: ``}
|
||||
${!this.hide && view?.is('folder')
|
||||
? html` <advanced-camera-card-folder-gallery
|
||||
.hass=${this.hass}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.viewItemManager=${this.viewItemManager}
|
||||
.galleryConfig=${this.config.media_gallery}
|
||||
></advanced-camera-card-folder-gallery>`
|
||||
: ``}
|
||||
${
|
||||
// Note: Subtle difference in condition below vs the other views in
|
||||
// order to always render the live view for live.preload mode.
|
||||
|
||||
Reference in New Issue
Block a user