@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user