Add download control to thumbnails.

This commit is contained in:
Dermot Duffy
2023-03-12 20:28:46 -07:00
parent f53490a70c
commit e1f146b560
17 changed files with 354 additions and 216 deletions
+2
View File
@@ -464,6 +464,8 @@ export class FrigateCardGalleryCore extends LitElement {
.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.view && this._media) {
this.view
+6 -4
View File
@@ -190,11 +190,13 @@ export function dispatchErrorMessageEvent(
*/
export function dispatchFrigateCardErrorEvent(
element: EventTarget,
error: FrigateCardError | Error,
error: unknown,
): void {
dispatchErrorMessageEvent(element, error.message, {
...(error instanceof FrigateCardError && { context: error.context }),
});
if (error instanceof Error) {
dispatchErrorMessageEvent(element, error.message, {
...(error instanceof FrigateCardError && { context: error.context }),
});
}
}
declare global {
+1
View File
@@ -163,6 +163,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
?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) => {
if (this.view && this.view.queryResults) {
dispatchFrigateCardEvent<ThumbnailCarouselTap>(
+177 -105
View File
@@ -30,6 +30,8 @@ import type { ExtendedHomeAssistant } from '../types.js';
import { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
import { CameraManager } from '../camera-manager/manager.js';
import { ViewMediaClassifier } from '../view/media-classifier.js';
import { downloadMedia } from '../utils/download.js';
import { dispatchFrigateCardErrorEvent } from './message.js';
// The minimum width of a thumbnail with details enabled.
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
@@ -162,56 +164,78 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
if (!this.media) {
return;
}
const score = this.media.getScore();
const startTime = this.media.getStartTime();
const endTime = this.media.getEndTime();
const what = this.media.getWhat();
const where = this.media.getWhere();
const tags = this.media.getTags();
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 endTime = rawStartTime
? rawEndTime
? getDurationString(rawStartTime, rawEndTime)
: 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`
${what
? html` <div class="title" title=${localize('event.what')}>
${prettifyTitle(what.join(', ')) +
(tags ? ': ' + prettifyTitle(tags.join(', ')) : '')}
${score ? html`(${(score * 100).toFixed(2) + '%'})` : ''}
${whatWithTags
? html` <div class="title">
<span title=${whatWithTags}>${whatWithTags}</span>
${score ? html`<span title="${score}">${score}</span>` : ''}
</div>`
: ``}
<div class="details">
${startTime
? html` <div title=${localize('event.start')}>
<ha-icon .icon=${'mdi:calendar-clock-outline'}></ha-icon>
${formatDateAndTime(startTime)}
? html` <div>
<ha-icon
title=${localize('event.start')}
.icon=${'mdi:calendar-clock-outline'}
></ha-icon>
<span title="${startTime}">${startTime}</span>
</div>
<div title=${localize('event.duration')}>
<ha-icon .icon=${'mdi:clock-outline'}></ha-icon>
${endTime
? getDurationString(startTime, endTime)
: localize('event.in_progress')}
<div>
<ha-icon
title=${localize('event.duration')}
.icon=${'mdi:clock-outline'}
></ha-icon>
<span title="${endTime}">${endTime}</span>
</div>`
: ''}
${this.cameraTitle
? html` <div title=${localize('event.camera')}>
<ha-icon .icon=${'mdi:cctv'}></ha-icon>
${this.cameraTitle}
? html` <div>
<ha-icon title=${localize('event.camera')} .icon=${'mdi:cctv'}></ha-icon>
<span title="${this.cameraTitle}">${this.cameraTitle}</span>
</div>`
: ''}
${where
? html` <div title=${localize('event.where')}>
<ha-icon .icon=${'mdi:map-marker-outline'}></ha-icon>
${prettifyTitle(where.join(', '))}
? html` <div>
<ha-icon
title=${localize('event.where')}
.icon=${'mdi:map-marker-outline'}
></ha-icon>
<span title="${where}">${where}</span>
</div>`
: html``}
${tags
? html` <div title=${localize('event.tag')}>
<ha-icon .icon=${'mdi:tag'}></ha-icon>
${prettifyTitle(tags.join(', '))}
? html` <div>
<ha-icon title=${localize('event.tag')} .icon=${'mdi:tag'}></ha-icon>
<span title="${tags}">${tags}</span>
</div>`
: html``}
${this.seek
? html` <div title=${localize('event.seek')}>
<ha-icon .icon=${'mdi:clock-fast'}></ha-icon>
${format(this.seek, 'HH:mm:ss')}
${seek
? html` <div>
<ha-icon
title=${localize('event.seek')}
.icon=${'mdi:clock-fast'}
></ha-icon>
<span title="${seek}">${seek}</span>
</div>`
: html``}
</div>
@@ -238,38 +262,56 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
if (!this.media) {
return;
}
const startTime = this.media.getStartTime();
const endTime = this.media.getEndTime();
const rawStartTime = this.media.getStartTime();
const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
const rawEndTime = this.media.getEndTime();
const endTime = rawStartTime
? rawEndTime
? getDurationString(rawStartTime, rawEndTime)
: localize('event.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" title=${localize('recording.camera')}>
${this.cameraTitle}
? html` <div class="title">
<span title="${this.cameraTitle}">${this.cameraTitle}</span>
</div>`
: ``}
<div class="details">
${startTime
? html` <div title=${localize('recording.start')}>
<ha-icon .icon=${'mdi:calendar-clock-outline'}></ha-icon>
${formatDateAndTime(startTime)}
? html` <div>
<ha-icon title=${localize(
'recording.start',
)} .icon=${'mdi:calendar-clock-outline'}></ha-icon>
<span title="${startTime}">${startTime}</span>
</div>
<div title=${localize('recording.duration')}>
<ha-icon .icon=${'mdi:clock-outline'}></ha-icon>
${endTime
? getDurationString(startTime, endTime)
: localize('event.in_progress')}
<div>
<ha-icon title=${localize(
'recording.duration',
)} .icon=${'mdi:clock-outline'}></ha-icon>
<span title="${endTime}">${endTime}</span>
</div>`
: ''}
${this.seek
? html` <div title=${localize('event.seek')}>
<ha-icon .icon=${'mdi:clock-fast'}></ha-icon>
${format(this.seek, 'HH:mm:ss')}
${seek
? html` <div>
<ha-icon
title=${localize('event.seek')}
.icon=${'mdi:clock-fast'}
></ha-icon>
<span title="${seek}">${seek}</span>
</div>`
: html``}
${eventCount !== null
? html`<div title=${localize('recording.events')}>
<ha-icon .icon=${'mdi:shield-alert'}></ha-icon>
${eventCount}
? html`<div>
<ha-icon
title=${localize('recording.events')}
.icon=${'mdi:shield-alert'}
></ha-icon>
<span title="${eventCount}">${eventCount}</span>
</div>`
: ``}
</div>
@@ -303,6 +345,9 @@ export class FrigateCardThumbnail extends LitElement {
@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;
@@ -333,34 +378,43 @@ export class FrigateCardThumbnail extends LitElement {
// 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 &&
this.cameraManager?.getMediaCapabilities(this.media)?.canFavorite;
mediaCapabilities?.canFavorite;
const shouldShowDownloadControl =
this.show_download_control &&
this.hass &&
this.media.getID() &&
mediaCapabilities?.canDownload;
const cameraTitle = this.cameraManager.getCameraMetadata(
this.hass,
this.media.getCameraID(),
)?.title;
return html` ${ViewMediaClassifier.isEvent(this.media)
? html`<frigate-card-thumbnail-feature-event
aria-label="${title ?? ''}"
title=${title}
.hass=${this.hass}
.thumbnail=${thumbnail ?? undefined}
></frigate-card-thumbnail-feature-event>`
: ViewMediaClassifier.isRecording(this.media)
? html`<frigate-card-thumbnail-feature-recording
aria-label="${title ?? ''}"
title="${title ?? ''}"
.cameraTitle=${this.details ? undefined : cameraTitle}
.date=${this.media.getStartTime() ?? undefined}
></frigate-card-thumbnail-feature-recording>`
: html``}
${shouldShowFavoriteControl
? html` <ha-icon
return html`
${ViewMediaClassifier.isEvent(this.media)
? html`<frigate-card-thumbnail-feature-event
aria-label="${title ?? ''}"
title=${title}
.hass=${this.hass}
.thumbnail=${thumbnail ?? undefined}
></frigate-card-thumbnail-feature-event>`
: ViewMediaClassifier.isRecording(this.media)
? html`<frigate-card-thumbnail-feature-recording
aria-label="${title ?? ''}"
title="${title ?? ''}"
.cameraTitle=${this.details ? undefined : cameraTitle}
.date=${this.media.getStartTime() ?? undefined}
></frigate-card-thumbnail-feature-recording>`
: html``}
${shouldShowFavoriteControl
? html` <ha-icon
class="${classMap(starClasses)}"
icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'}
title=${localize('thumbnail.retain_indefinitely')}
@@ -381,42 +435,60 @@ export class FrigateCardThumbnail extends LitElement {
}
}}
/></ha-icon>`
: ``}
${this.details && ViewMediaClassifier.isEvent(this.media)
? html`<frigate-card-thumbnail-details-event
.media=${this.media ?? undefined}
.cameraTitle=${cameraTitle}
.seek=${this.seek}
></frigate-card-thumbnail-details-event>`
: this.details && ViewMediaClassifier.isRecording(this.media)
? html`<frigate-card-thumbnail-details-recording
.media=${this.media ?? undefined}
.cameraTitle=${cameraTitle}
.seek=${this.seek}
></frigate-card-thumbnail-details-recording>`
: html``}
${shouldShowTimelineControl
? html`<ha-icon
class="timeline"
icon="mdi:target"
title=${localize('thumbnail.timeline')}
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (!this.view || !this.media) {
return;
}
this.view
.evolve({
view: 'timeline',
queryResults: this.view.queryResults
?.clone()
.selectResultIfFound((media) => media === this.media),
})
.removeContext('timeline')
.dispatchChangeEvent(this);
}}
></ha-icon>`
: ''}`;
: ``}
${this.details && ViewMediaClassifier.isEvent(this.media)
? html`<frigate-card-thumbnail-details-event
.media=${this.media ?? undefined}
.cameraTitle=${cameraTitle}
.seek=${this.seek}
></frigate-card-thumbnail-details-event>`
: this.details && ViewMediaClassifier.isRecording(this.media)
? html`<frigate-card-thumbnail-details-recording
.media=${this.media ?? undefined}
.cameraTitle=${cameraTitle}
.seek=${this.seek}
></frigate-card-thumbnail-details-recording>`
: html``}
${shouldShowTimelineControl
? html`<ha-icon
class="timeline"
icon="mdi:target"
title=${localize('thumbnail.timeline')}
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (!this.view || !this.media) {
return;
}
this.view
.evolve({
view: 'timeline',
queryResults: this.view.queryResults
?.clone()
.selectResultIfFound((media) => media === this.media),
})
.removeContext('timeline')
.dispatchChangeEvent(this);
}}
></ha-icon>`
: ''}
${shouldShowDownloadControl
? html` <ha-icon
class="download"
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) {
dispatchFrigateCardErrorEvent(this, error);
}
}
}}
></ha-icon>`
: ``}
`;
}
/**