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
+11 -2
View File
@@ -428,6 +428,7 @@ live:
| `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :white_check_mark: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :white_check_mark: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`. | | `size` | 100 | :white_check_mark: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`. |
| `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :white_check_mark: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_download_control` | `true` | :white_check_mark: | Whether to show the download control on each thumbnail.|
| `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.| | `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.| | `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.|
| `media` | `all` | :white_check_mark: | Whether to show `clips`, `snapshots` or `all` in the thumbnail carousel in the `live` view.| | `media` | `all` | :white_check_mark: | Whether to show `clips`, `snapshots` or `all` in the thumbnail carousel in the `live` view.|
@@ -537,6 +538,7 @@ media_viewer:
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel pixels. Must be >= `75` and <= `175`.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_download_control` | `true` | :heavy_multiplication_x: | Whether to show the download control on each thumbnail.|
| `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.| | `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.| | `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.|
@@ -619,6 +621,7 @@ media_gallery:
| - | - | - | - | | - | - | - | - |
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the gallery. Must be >= `75` and <= `175`.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the gallery. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show media details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show media details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_download_control` | `true` | :heavy_multiplication_x: | Whether to show the download control on each thumbnail.|
| `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.| | `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.| | `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.|
@@ -680,8 +683,9 @@ timeline:
| `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).| | `mode` | `none` | :heavy_multiplication_x: | Whether to show the thumbnail carousel `below` the media, `above` the media, in a drawer to the `left` or `right` of the media or to hide it entirely (`none`).|
| `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`.| | `size` | 100 | :heavy_multiplication_x: | The size of the thumbnails in the thumbnail carousel in pixels. Must be >= `75` and <= `175`.|
| `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.| | `show_details` | `false` | :heavy_multiplication_x: | Whether to show event details (e.g. duration, start time, object detected, etc) alongside the thumbnail.|
| `show_favorite_control` | `true` | :white_check_mark: | Whether to show the favorite ('star') control on each thumbnail.| | `show_download_control` | `true` | :heavy_multiplication_x: | Whether to show the download control on each thumbnail.|
| `show_timeline_control` | `true` | :white_check_mark: | Whether to show the timeline ('target') control on each thumbnail.| | `show_favorite_control` | `true` | :heavy_multiplication_x: | Whether to show the favorite ('star') control on each thumbnail.|
| `show_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.|
<a name="dimensions"></a> <a name="dimensions"></a>
@@ -1646,6 +1650,7 @@ live:
media: clips media: clips
size: 100 size: 100
show_details: false show_details: false
show_download_control: true
show_favorite_control: true show_favorite_control: true
show_timeline_control: true show_timeline_control: true
mode: none mode: none
@@ -1704,6 +1709,7 @@ media_viewer:
size: 100 size: 100
mode: none mode: none
show_details: false show_details: false
show_download_control: true
show_favorite_control: true show_favorite_control: true
show_timeline_control: true show_timeline_control: true
timeline: timeline:
@@ -1750,6 +1756,7 @@ media_gallery:
thumbnails: thumbnails:
size: 100 size: 100
show_details: false show_details: false
show_download_control: true
show_favorite_control: true show_favorite_control: true
show_timeline_control: true show_timeline_control: true
actions: actions:
@@ -2174,6 +2181,7 @@ timeline:
mode: left mode: left
size: 100 size: 100
show_details: true show_details: true
show_download_control: true
show_favorite_control: true show_favorite_control: true
show_timeline_control: true show_timeline_control: true
``` ```
@@ -2284,6 +2292,7 @@ overrides:
media: clips media: clips
size: 100 size: 100
show_details: false show_details: false
show_download_control: true
show_favorite_control: true show_favorite_control: true
show_timeline_control: true show_timeline_control: true
mode: none mode: none
@@ -1073,6 +1073,7 @@ export class FrigateCameraManagerEngine
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities { public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
return { return {
canFavorite: ViewMediaClassifier.isEvent(media), canFavorite: ViewMediaClassifier.isEvent(media),
canDownload: true,
}; };
} }
+1
View File
@@ -104,6 +104,7 @@ export type CameraManagerCapabilities = BaseCapabilities;
export type CameraManagerCameraCapabilities = BaseCapabilities; export type CameraManagerCameraCapabilities = BaseCapabilities;
export interface CameraManagerMediaCapabilities { export interface CameraManagerMediaCapabilities {
canFavorite: boolean; canFavorite: boolean;
canDownload: boolean;
} }
export interface CameraManagerCameraMetadata { export interface CameraManagerCameraMetadata {
+30 -75
View File
@@ -39,20 +39,20 @@ import {
Actions, Actions,
ActionType, ActionType,
CameraConfig, CameraConfig,
CardWideConfig,
ExtendedHomeAssistant, ExtendedHomeAssistant,
FRIGATE_CARD_VIEW_DEFAULT,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig, FrigateCardConfig,
frigateCardConfigSchema, frigateCardConfigSchema,
FrigateCardCustomAction, FrigateCardCustomAction,
FrigateCardError,
FrigateCardView, FrigateCardView,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MediaLoadedInfo, MediaLoadedInfo,
MESSAGE_TYPE_PRIORITIES,
MenuButton, MenuButton,
MESSAGE_TYPE_PRIORITIES,
Message, Message,
RawFrigateCardConfig, RawFrigateCardConfig,
CardWideConfig,
FrigateCardError,
FRIGATE_CARD_VIEW_DEFAULT,
} from './types.js'; } from './types.js';
import { import {
convertActionToFrigateCardCustomAction, convertActionToFrigateCardCustomAction,
@@ -66,7 +66,6 @@ import {
getEntityIcon, getEntityIcon,
getEntityTitle, getEntityTitle,
getHassDifferences, getHassDifferences,
homeAssistantSignPath,
isCardInPanel, isCardInPanel,
isHassDifferent, isHassDifferent,
isTriggeredState, isTriggeredState,
@@ -92,6 +91,7 @@ import isEqual from 'lodash-es/isEqual';
import merge from 'lodash-es/merge'; import merge from 'lodash-es/merge';
import { FrigateCardInitializer } from './utils/initializer.js'; import { FrigateCardInitializer } from './utils/initializer.js';
import 'web-dialog'; import 'web-dialog';
import { downloadMedia } from './utils/download.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
@@ -409,10 +409,14 @@ class FrigateCard extends LitElement {
this._cameraManager, this._cameraManager,
selectedCameraID, selectedCameraID,
); );
const selectedMedia = this._view?.queryResults?.getSelectedResult();
const cameraCapabilities = allSelectedCameraIDs const cameraCapabilities = allSelectedCameraIDs
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs) ? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
: null; : null;
const mediaCapabilities = selectedMedia
? this._cameraManager?.getMediaCapabilities(selectedMedia)
: null;
buttons.push({ buttons.push({
// Use a magic icon value that the menu will use to render the custom // Use a magic icon value that the menu will use to render the custom
@@ -588,11 +592,7 @@ class FrigateCard extends LitElement {
}); });
} }
if ( if (mediaCapabilities?.canDownload && !this._isBeingCasted()) {
!this._isBeingCasted() &&
(this._view?.isViewerView() ||
(this._view?.is('timeline') && !!this._view?.queryResults?.hasSelectedResult()))
) {
buttons.push({ buttons.push({
icon: 'mdi:download', icon: 'mdi:download',
...this._getConfig().menu.buttons.download, ...this._getConfig().menu.buttons.download,
@@ -1090,6 +1090,19 @@ class FrigateCard extends LitElement {
} }
} }
protected _handleThrownError(error: unknown) {
if (error instanceof Error) {
errorToConsole(error);
}
if (error instanceof FrigateCardError) {
this._setMessageAndUpdate({
message: error.message,
type: 'error',
context: error.context,
});
}
}
protected async _initializeCameras( protected async _initializeCameras(
hass: HomeAssistant, hass: HomeAssistant,
config: FrigateCardConfig, config: FrigateCardConfig,
@@ -1115,16 +1128,7 @@ class FrigateCard extends LitElement {
cameras, cameras,
); );
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof Error) { this._handleThrownError(e);
errorToConsole(e);
}
if (e instanceof FrigateCardError) {
this._setMessageAndUpdate({
message: e.message,
type: 'error',
context: e.context,
});
}
} }
// If there's no view set yet, set one. This will be the case on initial camera load. // If there's no view set yet, set one. This will be the case on initial camera load.
@@ -1319,64 +1323,15 @@ class FrigateCard extends LitElement {
* Download media being displayed in the viewer. * Download media being displayed in the viewer.
*/ */
protected async _downloadViewerMedia(): Promise<void> { protected async _downloadViewerMedia(): Promise<void> {
if (!this._hass || !(this._view?.isViewerView() || this._view?.is('timeline'))) { const media = this._view?.queryResults?.getSelectedResult();
// Should not occur. if (!this._hass || !this._cameraManager || !media) {
return;
}
const media = this._view.queryResults?.getSelectedResult();
if (!media) {
this._setMessageAndUpdate({
message: localize('error.download_no_media'),
type: 'error',
});
return; return;
} }
const cameraConfig = this._getSelectedCameraConfig();
if (!cameraConfig || !cameraConfig.frigate.camera_name) {
return;
}
const path = this._cameraManager?.getMediaDownloadPath(media);
if (!path) {
return;
}
let response: string | null | undefined;
try { try {
response = await homeAssistantSignPath(this._hass, path); await downloadMedia(this._hass, this._cameraManager, media);
} catch (e) { } catch (error) {
errorToConsole(e as Error); this._handleThrownError(error);
}
if (!response) {
this._setMessageAndUpdate({
message: localize('error.download_sign_failed'),
type: 'error',
});
return;
}
if (
navigator.userAgent.startsWith('Home Assistant/') ||
navigator.userAgent.startsWith('HomeAssistant/')
) {
// Home Assistant companion apps cannot download files without opening a
// new browser window.
//
// User-agents are specified here:
// - Android: https://github.com/home-assistant/android/blob/master/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt#L107
// - iOS: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift#L75
window.open(response, '_blank');
} else {
// Use the HTML5 download attribute to prevent a new window from
// temporarily opening.
const link = document.createElement('a');
link.setAttribute('download', '');
link.href = response;
link.click();
link.remove();
} }
} }
+2
View File
@@ -464,6 +464,8 @@ export class FrigateCardGalleryCore extends LitElement {
.show_favorite_control} .show_favorite_control}
?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails ?show_timeline_control=${!!this.galleryConfig?.controls.thumbnails
.show_timeline_control} .show_timeline_control}
?show_download_control=${!!this.galleryConfig?.controls.thumbnails
.show_download_control}
@click=${(ev: Event) => { @click=${(ev: Event) => {
if (this.view && this._media) { if (this.view && this._media) {
this.view this.view
+6 -4
View File
@@ -190,11 +190,13 @@ export function dispatchErrorMessageEvent(
*/ */
export function dispatchFrigateCardErrorEvent( export function dispatchFrigateCardErrorEvent(
element: EventTarget, element: EventTarget,
error: FrigateCardError | Error, error: unknown,
): void { ): void {
dispatchErrorMessageEvent(element, error.message, { if (error instanceof Error) {
...(error instanceof FrigateCardError && { context: error.context }), dispatchErrorMessageEvent(element, error.message, {
}); ...(error instanceof FrigateCardError && { context: error.context }),
});
}
} }
declare global { declare global {
+1
View File
@@ -163,6 +163,7 @@ export class FrigateCardThumbnailCarousel extends LitElement {
?details=${!!this.config?.show_details} ?details=${!!this.config?.show_details}
?show_favorite_control=${this.config?.show_favorite_control} ?show_favorite_control=${this.config?.show_favorite_control}
?show_timeline_control=${this.config?.show_timeline_control} ?show_timeline_control=${this.config?.show_timeline_control}
?show_download_control=${this.config?.show_download_control}
@click=${(ev: Event) => { @click=${(ev: Event) => {
if (this.view && this.view.queryResults) { if (this.view && this.view.queryResults) {
dispatchFrigateCardEvent<ThumbnailCarouselTap>( 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 { EventViewMedia, RecordingViewMedia, ViewMedia } from '../view/media.js';
import { CameraManager } from '../camera-manager/manager.js'; import { CameraManager } from '../camera-manager/manager.js';
import { ViewMediaClassifier } from '../view/media-classifier.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. // The minimum width of a thumbnail with details enabled.
export const THUMBNAIL_DETAILS_WIDTH_MIN = 300; export const THUMBNAIL_DETAILS_WIDTH_MIN = 300;
@@ -162,56 +164,78 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
if (!this.media) { if (!this.media) {
return; return;
} }
const score = this.media.getScore(); const rawScore = this.media.getScore();
const startTime = this.media.getStartTime(); const score = rawScore ? (rawScore * 100).toFixed(2) + '%' : null;
const endTime = this.media.getEndTime(); const rawStartTime = this.media.getStartTime();
const what = this.media.getWhat(); const startTime = rawStartTime ? formatDateAndTime(rawStartTime) : null;
const where = this.media.getWhere();
const tags = this.media.getTags(); 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` return html`
${what ${whatWithTags
? html` <div class="title" title=${localize('event.what')}> ? html` <div class="title">
${prettifyTitle(what.join(', ')) + <span title=${whatWithTags}>${whatWithTags}</span>
(tags ? ': ' + prettifyTitle(tags.join(', ')) : '')} ${score ? html`<span title="${score}">${score}</span>` : ''}
${score ? html`(${(score * 100).toFixed(2) + '%'})` : ''}
</div>` </div>`
: ``} : ``}
<div class="details"> <div class="details">
${startTime ${startTime
? html` <div title=${localize('event.start')}> ? html` <div>
<ha-icon .icon=${'mdi:calendar-clock-outline'}></ha-icon> <ha-icon
${formatDateAndTime(startTime)} title=${localize('event.start')}
.icon=${'mdi:calendar-clock-outline'}
></ha-icon>
<span title="${startTime}">${startTime}</span>
</div> </div>
<div title=${localize('event.duration')}> <div>
<ha-icon .icon=${'mdi:clock-outline'}></ha-icon> <ha-icon
${endTime title=${localize('event.duration')}
? getDurationString(startTime, endTime) .icon=${'mdi:clock-outline'}
: localize('event.in_progress')} ></ha-icon>
<span title="${endTime}">${endTime}</span>
</div>` </div>`
: ''} : ''}
${this.cameraTitle ${this.cameraTitle
? html` <div title=${localize('event.camera')}> ? html` <div>
<ha-icon .icon=${'mdi:cctv'}></ha-icon> <ha-icon title=${localize('event.camera')} .icon=${'mdi:cctv'}></ha-icon>
${this.cameraTitle} <span title="${this.cameraTitle}">${this.cameraTitle}</span>
</div>` </div>`
: ''} : ''}
${where ${where
? html` <div title=${localize('event.where')}> ? html` <div>
<ha-icon .icon=${'mdi:map-marker-outline'}></ha-icon> <ha-icon
${prettifyTitle(where.join(', '))} title=${localize('event.where')}
.icon=${'mdi:map-marker-outline'}
></ha-icon>
<span title="${where}">${where}</span>
</div>` </div>`
: html``} : html``}
${tags ${tags
? html` <div title=${localize('event.tag')}> ? html` <div>
<ha-icon .icon=${'mdi:tag'}></ha-icon> <ha-icon title=${localize('event.tag')} .icon=${'mdi:tag'}></ha-icon>
${prettifyTitle(tags.join(', '))} <span title="${tags}">${tags}</span>
</div>` </div>`
: html``} : html``}
${this.seek ${seek
? html` <div title=${localize('event.seek')}> ? html` <div>
<ha-icon .icon=${'mdi:clock-fast'}></ha-icon> <ha-icon
${format(this.seek, 'HH:mm:ss')} title=${localize('event.seek')}
.icon=${'mdi:clock-fast'}
></ha-icon>
<span title="${seek}">${seek}</span>
</div>` </div>`
: html``} : html``}
</div> </div>
@@ -238,38 +262,56 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
if (!this.media) { if (!this.media) {
return; return;
} }
const startTime = this.media.getStartTime(); const rawStartTime = this.media.getStartTime();
const endTime = this.media.getEndTime(); 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(); const eventCount = this.media.getEventCount();
return html` return html`
${this.cameraTitle ${this.cameraTitle
? html` <div class="title" title=${localize('recording.camera')}> ? html` <div class="title">
${this.cameraTitle} <span title="${this.cameraTitle}">${this.cameraTitle}</span>
</div>` </div>`
: ``} : ``}
<div class="details"> <div class="details">
${startTime ${startTime
? html` <div title=${localize('recording.start')}> ? html` <div>
<ha-icon .icon=${'mdi:calendar-clock-outline'}></ha-icon> <ha-icon title=${localize(
${formatDateAndTime(startTime)} 'recording.start',
)} .icon=${'mdi:calendar-clock-outline'}></ha-icon>
<span title="${startTime}">${startTime}</span>
</div> </div>
<div title=${localize('recording.duration')}> <div>
<ha-icon .icon=${'mdi:clock-outline'}></ha-icon> <ha-icon title=${localize(
${endTime 'recording.duration',
? getDurationString(startTime, endTime) )} .icon=${'mdi:clock-outline'}></ha-icon>
: localize('event.in_progress')} <span title="${endTime}">${endTime}</span>
</div>` </div>`
: ''} : ''}
${this.seek ${seek
? html` <div title=${localize('event.seek')}> ? html` <div>
<ha-icon .icon=${'mdi:clock-fast'}></ha-icon> <ha-icon
${format(this.seek, 'HH:mm:ss')} title=${localize('event.seek')}
.icon=${'mdi:clock-fast'}
></ha-icon>
<span title="${seek}">${seek}</span>
</div>` </div>`
: html``} : html``}
${eventCount !== null ${eventCount !== null
? html`<div title=${localize('recording.events')}> ? html`<div>
<ha-icon .icon=${'mdi:shield-alert'}></ha-icon> <ha-icon
${eventCount} title=${localize('recording.events')}
.icon=${'mdi:shield-alert'}
></ha-icon>
<span title="${eventCount}">${eventCount}</span>
</div>` </div>`
: ``} : ``}
</div> </div>
@@ -303,6 +345,9 @@ export class FrigateCardThumbnail extends LitElement {
@property({ attribute: true, type: Boolean }) @property({ attribute: true, type: Boolean })
public show_timeline_control = false; public show_timeline_control = false;
@property({ attribute: true, type: Boolean })
public show_download_control = false;
@property({ attribute: false }) @property({ attribute: false })
public seek?: Date; public seek?: Date;
@@ -333,34 +378,43 @@ export class FrigateCardThumbnail extends LitElement {
// Only show timeline control if the recording has a start & end time. // Only show timeline control if the recording has a start & end time.
(this.media.getStartTime() && this.media.getEndTime())); (this.media.getStartTime() && this.media.getEndTime()));
const mediaCapabilities = this.cameraManager?.getMediaCapabilities(this.media);
const shouldShowFavoriteControl = const shouldShowFavoriteControl =
this.show_favorite_control && this.show_favorite_control &&
this.media && this.media &&
this.hass && 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( const cameraTitle = this.cameraManager.getCameraMetadata(
this.hass, this.hass,
this.media.getCameraID(), this.media.getCameraID(),
)?.title; )?.title;
return html` ${ViewMediaClassifier.isEvent(this.media) return html`
? html`<frigate-card-thumbnail-feature-event ${ViewMediaClassifier.isEvent(this.media)
aria-label="${title ?? ''}" ? html`<frigate-card-thumbnail-feature-event
title=${title} aria-label="${title ?? ''}"
.hass=${this.hass} title=${title}
.thumbnail=${thumbnail ?? undefined} .hass=${this.hass}
></frigate-card-thumbnail-feature-event>` .thumbnail=${thumbnail ?? undefined}
: ViewMediaClassifier.isRecording(this.media) ></frigate-card-thumbnail-feature-event>`
? html`<frigate-card-thumbnail-feature-recording : ViewMediaClassifier.isRecording(this.media)
aria-label="${title ?? ''}" ? html`<frigate-card-thumbnail-feature-recording
title="${title ?? ''}" aria-label="${title ?? ''}"
.cameraTitle=${this.details ? undefined : cameraTitle} title="${title ?? ''}"
.date=${this.media.getStartTime() ?? undefined} .cameraTitle=${this.details ? undefined : cameraTitle}
></frigate-card-thumbnail-feature-recording>` .date=${this.media.getStartTime() ?? undefined}
: html``} ></frigate-card-thumbnail-feature-recording>`
${shouldShowFavoriteControl : html``}
? html` <ha-icon ${shouldShowFavoriteControl
? html` <ha-icon
class="${classMap(starClasses)}" class="${classMap(starClasses)}"
icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'} icon=${this.media.isFavorite() ? 'mdi:star' : 'mdi:star-outline'}
title=${localize('thumbnail.retain_indefinitely')} title=${localize('thumbnail.retain_indefinitely')}
@@ -381,42 +435,60 @@ export class FrigateCardThumbnail extends LitElement {
} }
}} }}
/></ha-icon>` /></ha-icon>`
: ``} : ``}
${this.details && ViewMediaClassifier.isEvent(this.media) ${this.details && ViewMediaClassifier.isEvent(this.media)
? html`<frigate-card-thumbnail-details-event ? html`<frigate-card-thumbnail-details-event
.media=${this.media ?? undefined} .media=${this.media ?? undefined}
.cameraTitle=${cameraTitle} .cameraTitle=${cameraTitle}
.seek=${this.seek} .seek=${this.seek}
></frigate-card-thumbnail-details-event>` ></frigate-card-thumbnail-details-event>`
: this.details && ViewMediaClassifier.isRecording(this.media) : this.details && ViewMediaClassifier.isRecording(this.media)
? html`<frigate-card-thumbnail-details-recording ? html`<frigate-card-thumbnail-details-recording
.media=${this.media ?? undefined} .media=${this.media ?? undefined}
.cameraTitle=${cameraTitle} .cameraTitle=${cameraTitle}
.seek=${this.seek} .seek=${this.seek}
></frigate-card-thumbnail-details-recording>` ></frigate-card-thumbnail-details-recording>`
: html``} : html``}
${shouldShowTimelineControl ${shouldShowTimelineControl
? html`<ha-icon ? html`<ha-icon
class="timeline" class="timeline"
icon="mdi:target" icon="mdi:target"
title=${localize('thumbnail.timeline')} title=${localize('thumbnail.timeline')}
@click=${(ev: Event) => { @click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev); stopEventFromActivatingCardWideActions(ev);
if (!this.view || !this.media) { if (!this.view || !this.media) {
return; return;
} }
this.view this.view
.evolve({ .evolve({
view: 'timeline', view: 'timeline',
queryResults: this.view.queryResults queryResults: this.view.queryResults
?.clone() ?.clone()
.selectResultIfFound((media) => media === this.media), .selectResultIfFound((media) => media === this.media),
}) })
.removeContext('timeline') .removeContext('timeline')
.dispatchChangeEvent(this); .dispatchChangeEvent(this);
}} }}
></ha-icon>` ></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>`
: ``}
`;
} }
/** /**
+10 -3
View File
@@ -39,7 +39,7 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES = export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
`${CONF_CAMERAS}.#.triggers.entities` as const; `${CONF_CAMERAS}.#.triggers.entities` as const;
export const CONF_CAMERAS_GLOBAL = 'cameras_global' as const; const CONF_CAMERAS_GLOBAL = 'cameras_global' as const;
export const CONF_CAMERAS_GLOBAL_IMAGE = `${CONF_CAMERAS_GLOBAL}.image` as const; export const CONF_CAMERAS_GLOBAL_IMAGE = `${CONF_CAMERAS_GLOBAL}.image` as const;
export const CONF_CAMERAS_GLOBAL_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const; export const CONF_CAMERAS_GLOBAL_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const;
export const CONF_CAMERAS_GLOBAL_WEBRTC_CARD = export const CONF_CAMERAS_GLOBAL_WEBRTC_CARD =
@@ -58,7 +58,6 @@ export const CONF_VIEW_DEFAULT = `${CONF_VIEW}.default` as const;
export const CONF_VIEW_TIMEOUT_SECONDS = `${CONF_VIEW}.timeout_seconds` as const; export const CONF_VIEW_TIMEOUT_SECONDS = `${CONF_VIEW}.timeout_seconds` as const;
export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera` as const; export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera` as const;
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const; export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
export const CONF_VIEW_UPDATE_ENTITIES = `${CONF_VIEW}.update_entities` as const;
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const; export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const; export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const; export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
@@ -74,6 +73,8 @@ export const CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE =
`${CONF_MEDIA_GALLERY}.controls.filter.mode` as const; `${CONF_MEDIA_GALLERY}.controls.filter.mode` as const;
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS = export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_MEDIA_GALLERY}.controls.thumbnails.show_details` as const; `${CONF_MEDIA_GALLERY}.controls.thumbnails.show_details` as const;
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL =
`${CONF_MEDIA_GALLERY}.controls.thumbnails.show_download_control` as const;
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL = export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL =
`${CONF_MEDIA_GALLERY}.controls.thumbnails.show_favorite_control` as const; `${CONF_MEDIA_GALLERY}.controls.thumbnails.show_favorite_control` as const;
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL = export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
@@ -100,6 +101,8 @@ export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE =
`${CONF_MEDIA_VIEWER}.controls.thumbnails.mode` as const; `${CONF_MEDIA_VIEWER}.controls.thumbnails.mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS = export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_details` as const; `${CONF_MEDIA_VIEWER}.controls.thumbnails.show_details` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL =
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_download_control` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL = export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL =
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_favorite_control` as const; `${CONF_MEDIA_VIEWER}.controls.thumbnails.show_favorite_control` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL = export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
@@ -144,6 +147,8 @@ export const CONF_LIVE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_LIVE}.controls.thumbnails.size` as const; `${CONF_LIVE}.controls.thumbnails.size` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS = export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_LIVE}.controls.thumbnails.show_details` as const; `${CONF_LIVE}.controls.thumbnails.show_details` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL =
`${CONF_LIVE}.controls.thumbnails.show_download_control` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL = export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL =
`${CONF_LIVE}.controls.thumbnails.show_favorite_control` as const; `${CONF_LIVE}.controls.thumbnails.show_favorite_control` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL = export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
@@ -192,12 +197,14 @@ export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE =
`${CONF_TIMELINE}.controls.thumbnails.size` as const; `${CONF_TIMELINE}.controls.thumbnails.size` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS = export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${CONF_TIMELINE}.controls.thumbnails.show_details` as const; `${CONF_TIMELINE}.controls.thumbnails.show_details` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL =
`${CONF_TIMELINE}.controls.thumbnails.show_download_control` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL = export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL =
`${CONF_TIMELINE}.controls.thumbnails.show_favorite_control` as const; `${CONF_TIMELINE}.controls.thumbnails.show_favorite_control` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL = export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
`${CONF_TIMELINE}.controls.thumbnails.show_timeline_control` as const; `${CONF_TIMELINE}.controls.thumbnails.show_timeline_control` as const;
export const CONF_MENU = 'menu' as const; const CONF_MENU = 'menu' as const;
export const CONF_MENU_ALIGNMENT = `${CONF_MENU}.alignment` as const; export const CONF_MENU_ALIGNMENT = `${CONF_MENU}.alignment` as const;
export const CONF_MENU_POSITION = `${CONF_MENU}.position` as const; export const CONF_MENU_POSITION = `${CONF_MENU}.position` as const;
export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const; export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const;
+33 -16
View File
@@ -12,7 +12,6 @@ import {
upgradeConfig, upgradeConfig,
} from './config-mgmt.js'; } from './config-mgmt.js';
import { import {
CONF_CAMERAS,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
@@ -21,9 +20,13 @@ import {
CONF_CAMERAS_ARRAY_FRIGATE_LABELS, CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
CONF_CAMERAS_ARRAY_FRIGATE_URL, CONF_CAMERAS_ARRAY_FRIGATE_URL,
CONF_CAMERAS_ARRAY_FRIGATE_ZONES, CONF_CAMERAS_ARRAY_FRIGATE_ZONES,
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
CONF_CAMERAS_ARRAY_HIDE,
CONF_CAMERAS_ARRAY_ICON, CONF_CAMERAS_ARRAY_ICON,
CONF_CAMERAS_ARRAY_ID, CONF_CAMERAS_ARRAY_ID,
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS, CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
CONF_CAMERAS_ARRAY_IMAGE_URL,
CONF_CAMERAS_ARRAY_LIVE_PROVIDER, CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
CONF_CAMERAS_ARRAY_TITLE, CONF_CAMERAS_ARRAY_TITLE,
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES, CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
@@ -31,13 +34,9 @@ import {
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
CONF_DIMENSIONS_ASPECT_RATIO, CONF_CAMERAS,
CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_DIMENSIONS_ASPECT_RATIO_MODE,
CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE, CONF_DIMENSIONS_ASPECT_RATIO,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE,
CONF_IMAGE_LAYOUT_FIT, CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X, CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y, CONF_IMAGE_LAYOUT_POSITION_Y,
@@ -53,6 +52,7 @@ import {
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE, CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE, CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
@@ -72,15 +72,21 @@ import {
CONF_LIVE_PRELOAD, CONF_LIVE_PRELOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD, CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
CONF_LIVE_TRANSITION_EFFECT, CONF_LIVE_TRANSITION_EFFECT,
CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SIZE,
CONF_MEDIA_VIEWER_AUTO_MUTE, CONF_MEDIA_VIEWER_AUTO_MUTE,
CONF_MEDIA_VIEWER_AUTO_PAUSE, CONF_MEDIA_VIEWER_AUTO_PAUSE,
CONF_MEDIA_VIEWER_AUTO_PLAY, CONF_MEDIA_VIEWER_AUTO_PLAY,
CONF_MEDIA_VIEWER_AUTO_UNMUTE, CONF_MEDIA_VIEWER_AUTO_UNMUTE,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE,
@@ -96,19 +102,22 @@ import {
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X, CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y, CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
CONF_MEDIA_VIEWER_LAZY_LOAD, CONF_MEDIA_VIEWER_LAZY_LOAD,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT, CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_ALIGNMENT, CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS,
CONF_MENU_BUTTON_SIZE, CONF_MENU_BUTTON_SIZE,
CONF_MENU_BUTTONS,
CONF_MENU_POSITION, CONF_MENU_POSITION,
CONF_MENU_STYLE, CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR, CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_PROFILE, CONF_PERFORMANCE_PROFILE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS, CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW, CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CLUSTERING_THRESHOLD, CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
@@ -118,29 +127,24 @@ import {
CONF_VIEW_CAMERA_SELECT, CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DARK_MODE, CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT, CONF_VIEW_DEFAULT,
CONF_VIEW_SCAN,
CONF_VIEW_SCAN_ENABLED, CONF_VIEW_SCAN_ENABLED,
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS, CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
CONF_VIEW_SCAN_UNTRIGGER_RESET, CONF_VIEW_SCAN_UNTRIGGER_RESET,
CONF_VIEW_SCAN_UNTRIGGER_SECONDS, CONF_VIEW_SCAN_UNTRIGGER_SECONDS,
CONF_VIEW_SCAN,
CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS, CONF_VIEW_UPDATE_SECONDS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
MEDIA_CHUNK_SIZE_MAX, MEDIA_CHUNK_SIZE_MAX,
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
CONF_CAMERAS_ARRAY_GO2RTC_STREAM,
CONF_CAMERAS_ARRAY_HIDE,
CONF_CAMERAS_ARRAY_IMAGE_URL,
} from './const.js'; } from './const.js';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss'; import frigate_card_editor_style from './scss/editor.scss';
import { import {
BUTTON_SIZE_MIN, BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
FrigateCardConfig, FrigateCardConfig,
frigateCardConfigDefaults, frigateCardConfigDefaults,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig, RawFrigateCardConfig,
RawFrigateCardConfigArray, RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MAX,
@@ -1072,10 +1076,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathShowDetails: string, configPathShowDetails: string,
configPathShowFavoriteControl: string, configPathShowFavoriteControl: string,
configPathShowTimelineControl: string, configPathShowTimelineControl: string,
configPathShowDownloadControl: string,
defaults: { defaults: {
show_details: boolean; show_details: boolean;
show_favorite_control: boolean; show_favorite_control: boolean;
show_timeline_control: boolean; show_timeline_control: boolean;
show_download_control: boolean;
}, },
options?: { options?: {
configPathMedia?: string; configPathMedia?: string;
@@ -1128,6 +1134,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
label: localize('config.common.controls.thumbnails.show_timeline_control'), label: localize('config.common.controls.thumbnails.show_timeline_control'),
}, },
)} )}
${this._renderSwitch(
configPathShowDownloadControl,
defaults.show_download_control,
{
label: localize('config.common.controls.thumbnails.show_download_control'),
},
)}
`, `,
); );
} }
@@ -1733,6 +1746,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.live.controls.thumbnails, this._defaults.live.controls.thumbnails,
{ {
configPathMedia: CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, configPathMedia: CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
@@ -1774,6 +1788,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.media_gallery.controls.thumbnails, this._defaults.media_gallery.controls.thumbnails,
)} )}
${this._renderFilterControls( ${this._renderFilterControls(
@@ -1837,6 +1852,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.media_viewer.controls.thumbnails, this._defaults.media_viewer.controls.thumbnails,
{ {
configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE, configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
@@ -1898,6 +1914,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.timeline.controls.thumbnails, this._defaults.timeline.controls.thumbnails,
{ {
configPathMode: CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, configPathMode: CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
+4 -2
View File
@@ -148,6 +148,7 @@
"right": "Thumbnails in a drawer to the right" "right": "Thumbnails in a drawer to the right"
}, },
"show_details": "Show details with thumbnails", "show_details": "Show details with thumbnails",
"show_download_control": "Show download control on thumbnails",
"show_favorite_control": "Show favorite control on thumbnails", "show_favorite_control": "Show favorite control on thumbnails",
"show_timeline_control": "Show timeline control on thumbnails", "show_timeline_control": "Show timeline control on thumbnails",
"size": "Thumbnails size in pixels" "size": "Thumbnails size in pixels"
@@ -440,9 +441,10 @@
"start": "Start" "start": "Start"
}, },
"thumbnail": { "thumbnail": {
"download": "Download media",
"no_thumbnail": "No thumbnail available", "no_thumbnail": "No thumbnail available",
"retain_indefinitely": "Event will be indefinitely retained", "retain_indefinitely": "Media will be indefinitely retained",
"timeline": "See event/recording in timeline" "timeline": "See media in timeline"
}, },
"timeline": { "timeline": {
"lock": "Lock timeline to a single event", "lock": "Lock timeline to a single event",
+1
View File
@@ -148,6 +148,7 @@
"right": "Miniature in un cassetto a destra" "right": "Miniature in un cassetto a destra"
}, },
"show_details": "", "show_details": "",
"show_download_control": "",
"show_favorite_control": "Mostra il controllo preferito sulle miniature", "show_favorite_control": "Mostra il controllo preferito sulle miniature",
"show_timeline_control": "Mostra il controllo della sequenza temporale sulle miniature", "show_timeline_control": "Mostra il controllo della sequenza temporale sulle miniature",
"size": "Dimensione delle miniature in pixel" "size": "Dimensione delle miniature in pixel"
+1
View File
@@ -148,6 +148,7 @@
"right": "Miniaturas em uma gaveta à direita" "right": "Miniaturas em uma gaveta à direita"
}, },
"show_details": "", "show_details": "",
"show_download_control": "",
"show_favorite_control": "Mostrar controle de favorito nas miniaturas", "show_favorite_control": "Mostrar controle de favorito nas miniaturas",
"show_timeline_control": "Mostrar controle da linha do tempo nas miniaturas", "show_timeline_control": "Mostrar controle da linha do tempo nas miniaturas",
"size": "Tamanho das miniaturas em pixels" "size": "Tamanho das miniaturas em pixels"
+14 -6
View File
@@ -6,12 +6,12 @@ import {
} from './types'; } from './types';
import { getConfigValue, setConfigValue } from './config-mgmt.js'; import { getConfigValue, setConfigValue } from './config-mgmt.js';
import { import {
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_AUTO_MUTE, CONF_LIVE_AUTO_MUTE,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE, CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS, CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
@@ -20,18 +20,23 @@ import {
CONF_LIVE_LAZY_UNLOAD, CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD, CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
CONF_LIVE_TRANSITION_EFFECT, CONF_LIVE_TRANSITION_EFFECT,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_AUTO_MUTE, CONF_MEDIA_VIEWER_AUTO_MUTE,
CONF_MEDIA_VIEWER_AUTO_PAUSE, CONF_MEDIA_VIEWER_AUTO_PAUSE,
CONF_MEDIA_VIEWER_AUTO_PLAY, CONF_MEDIA_VIEWER_AUTO_PLAY,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE, CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE, CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT, CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_BUTTONS_FRIGATE, CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_MEDIA_PLAYER, CONF_MENU_BUTTONS_MEDIA_PLAYER,
@@ -43,11 +48,10 @@ import {
CONF_PERFORMANCE_STYLE_BOX_SHADOW, CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE, CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL, CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_SHOW_RECORDINGS, CONF_TIMELINE_SHOW_RECORDINGS,
CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
} from './const.js'; } from './const.js';
// Caution: These values are applied after parsing (since we cannot know the // Caution: These values are applied after parsing (since we cannot know the
@@ -103,15 +107,19 @@ const LOW_PROFILE_DEFAULTS = {
[`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false, [`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
// Disable all options in thumbnails. // Disable all options in thumbnails.
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false, [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false, [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false, [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
+7 -1
View File
@@ -33,7 +33,6 @@ ha-icon {
border-radius: 50%; border-radius: 50%;
opacity: 0.5; opacity: 0.5;
color: var(--primary-color); color: var(--primary-color);
top: 3px;
cursor: pointer; cursor: pointer;
@@ -44,6 +43,7 @@ ha-icon:hover {
} }
ha-icon.star { ha-icon.star {
top: 3px;
left: 3px; left: 3px;
} }
ha-icon.star.starred { ha-icon.star.starred {
@@ -51,9 +51,15 @@ ha-icon.star.starred {
} }
ha-icon.timeline { ha-icon.timeline {
top: 3px;
right: 3px; right: 3px;
} }
ha-icon.download {
right: 3px;
bottom: 3px;
}
frigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording { frigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording {
flex: 1; flex: 1;
} }
+6 -2
View File
@@ -245,7 +245,7 @@ const frigateCardLiveDependencySelectActionSchema =
frigate_card_action: z.literal('live_substream_select'), frigate_card_action: z.literal('live_substream_select'),
camera: z.string(), camera: z.string(),
}); });
const frigateCarMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({ const frigateCardMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('media_player'), frigate_card_action: z.literal('media_player'),
media_player: z.string(), media_player: z.string(),
media_player_action: z.enum(['play', 'stop']), media_player_action: z.enum(['play', 'stop']),
@@ -255,7 +255,7 @@ export const frigateCardCustomActionSchema = z.union([
frigateCardGeneralActionSchema, frigateCardGeneralActionSchema,
frigateCardCameraSelectActionSchema, frigateCardCameraSelectActionSchema,
frigateCardLiveDependencySelectActionSchema, frigateCardLiveDependencySelectActionSchema,
frigateCarMediaPlayerActionSchema, frigateCardMediaPlayerActionSchema,
]); ]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>; export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -747,6 +747,7 @@ const thumbnailControlsDefaults = {
show_details: true, show_details: true,
show_favorite_control: true, show_favorite_control: true,
show_timeline_control: true, show_timeline_control: true,
show_download_control: true,
}; };
const thumbnailsControlSchema = z.object({ const thumbnailsControlSchema = z.object({
@@ -765,6 +766,9 @@ const thumbnailsControlSchema = z.object({
show_timeline_control: z show_timeline_control: z
.boolean() .boolean()
.default(thumbnailControlsDefaults.show_timeline_control), .default(thumbnailControlsDefaults.show_timeline_control),
show_download_control: z
.boolean()
.default(thumbnailControlsDefaults.show_download_control),
}); });
export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>; export type ThumbnailsControlConfig = z.infer<typeof thumbnailsControlSchema>;
+49
View File
@@ -0,0 +1,49 @@
import { CameraManager } from '../camera-manager/manager';
import { localize } from '../localize/localize';
import { ExtendedHomeAssistant, FrigateCardError } from '../types';
import { ViewMedia } from '../view/media';
import { errorToConsole } from './basic';
import { homeAssistantSignPath } from './ha';
export const downloadMedia = async (
hass: ExtendedHomeAssistant,
cameraManager: CameraManager,
media: ViewMedia,
): Promise<void> => {
const path = cameraManager.getMediaDownloadPath(media);
if (!path) {
throw new FrigateCardError(localize('error.download_no_media'));
}
let response: string | null | undefined;
try {
response = await homeAssistantSignPath(hass, path);
} catch (e) {
errorToConsole(e as Error);
}
if (!response) {
throw new FrigateCardError(localize('error.download_sign_failed'));
}
if (
navigator.userAgent.startsWith('Home Assistant/') ||
navigator.userAgent.startsWith('HomeAssistant/')
) {
// Home Assistant companion apps cannot download files without opening a
// new browser window.
//
// User-agents are specified here:
// - Android: https://github.com/home-assistant/android/blob/master/app/src/main/java/io/homeassistant/companion/android/webview/WebViewActivity.kt#L107
// - iOS: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift#L75
window.open(response, '_blank');
} else {
// Use the HTML5 download attribute to prevent a new window from
// temporarily opening.
const link = document.createElement('a');
link.setAttribute('download', '');
link.href = response;
link.click();
link.remove();
}
};