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`).|
| `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_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_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.|
@@ -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`).|
| `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_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_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`.|
| `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_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`).|
| `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_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_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_timeline_control` | `true` | :heavy_multiplication_x: | Whether to show the timeline ('target') control on each thumbnail.|
<a name="dimensions"></a>
@@ -1646,6 +1650,7 @@ live:
media: clips
size: 100
show_details: false
show_download_control: true
show_favorite_control: true
show_timeline_control: true
mode: none
@@ -1704,6 +1709,7 @@ media_viewer:
size: 100
mode: none
show_details: false
show_download_control: true
show_favorite_control: true
show_timeline_control: true
timeline:
@@ -1750,6 +1756,7 @@ media_gallery:
thumbnails:
size: 100
show_details: false
show_download_control: true
show_favorite_control: true
show_timeline_control: true
actions:
@@ -2174,6 +2181,7 @@ timeline:
mode: left
size: 100
show_details: true
show_download_control: true
show_favorite_control: true
show_timeline_control: true
```
@@ -2284,6 +2292,7 @@ overrides:
media: clips
size: 100
show_details: false
show_download_control: true
show_favorite_control: true
show_timeline_control: true
mode: none
@@ -1073,6 +1073,7 @@ export class FrigateCameraManagerEngine
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
return {
canFavorite: ViewMediaClassifier.isEvent(media),
canDownload: true,
};
}
+1
View File
@@ -104,6 +104,7 @@ export type CameraManagerCapabilities = BaseCapabilities;
export type CameraManagerCameraCapabilities = BaseCapabilities;
export interface CameraManagerMediaCapabilities {
canFavorite: boolean;
canDownload: boolean;
}
export interface CameraManagerCameraMetadata {
+30 -75
View File
@@ -39,20 +39,20 @@ import {
Actions,
ActionType,
CameraConfig,
CardWideConfig,
ExtendedHomeAssistant,
FRIGATE_CARD_VIEW_DEFAULT,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
FrigateCardConfig,
frigateCardConfigSchema,
FrigateCardCustomAction,
FrigateCardError,
FrigateCardView,
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
MediaLoadedInfo,
MESSAGE_TYPE_PRIORITIES,
MenuButton,
MESSAGE_TYPE_PRIORITIES,
Message,
RawFrigateCardConfig,
CardWideConfig,
FrigateCardError,
FRIGATE_CARD_VIEW_DEFAULT,
} from './types.js';
import {
convertActionToFrigateCardCustomAction,
@@ -66,7 +66,6 @@ import {
getEntityIcon,
getEntityTitle,
getHassDifferences,
homeAssistantSignPath,
isCardInPanel,
isHassDifferent,
isTriggeredState,
@@ -92,6 +91,7 @@ import isEqual from 'lodash-es/isEqual';
import merge from 'lodash-es/merge';
import { FrigateCardInitializer } from './utils/initializer.js';
import 'web-dialog';
import { downloadMedia } from './utils/download.js';
/** A note on media callbacks:
*
@@ -409,10 +409,14 @@ class FrigateCard extends LitElement {
this._cameraManager,
selectedCameraID,
);
const selectedMedia = this._view?.queryResults?.getSelectedResult();
const cameraCapabilities = allSelectedCameraIDs
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
: null;
const mediaCapabilities = selectedMedia
? this._cameraManager?.getMediaCapabilities(selectedMedia)
: null;
buttons.push({
// Use a magic icon value that the menu will use to render the custom
@@ -588,11 +592,7 @@ class FrigateCard extends LitElement {
});
}
if (
!this._isBeingCasted() &&
(this._view?.isViewerView() ||
(this._view?.is('timeline') && !!this._view?.queryResults?.hasSelectedResult()))
) {
if (mediaCapabilities?.canDownload && !this._isBeingCasted()) {
buttons.push({
icon: 'mdi: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(
hass: HomeAssistant,
config: FrigateCardConfig,
@@ -1115,16 +1128,7 @@ class FrigateCard extends LitElement {
cameras,
);
} catch (e: unknown) {
if (e instanceof Error) {
errorToConsole(e);
}
if (e instanceof FrigateCardError) {
this._setMessageAndUpdate({
message: e.message,
type: 'error',
context: e.context,
});
}
this._handleThrownError(e);
}
// 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.
*/
protected async _downloadViewerMedia(): Promise<void> {
if (!this._hass || !(this._view?.isViewerView() || this._view?.is('timeline'))) {
// Should not occur.
return;
}
const media = this._view.queryResults?.getSelectedResult();
if (!media) {
this._setMessageAndUpdate({
message: localize('error.download_no_media'),
type: 'error',
});
const media = this._view?.queryResults?.getSelectedResult();
if (!this._hass || !this._cameraManager || !media) {
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 {
response = await homeAssistantSignPath(this._hass, path);
} catch (e) {
errorToConsole(e as 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();
await downloadMedia(this._hass, this._cameraManager, media);
} catch (error) {
this._handleThrownError(error);
}
}
+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>`
: ``}
`;
}
/**
+10 -3
View File
@@ -39,7 +39,7 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
`${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_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const;
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_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_ENTITIES = `${CONF_VIEW}.update_entities` 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_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;
export const CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${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 =
`${CONF_MEDIA_GALLERY}.controls.thumbnails.show_favorite_control` as const;
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;
export const CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${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 =
`${CONF_MEDIA_VIEWER}.controls.thumbnails.show_favorite_control` as const;
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;
export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${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 =
`${CONF_LIVE}.controls.thumbnails.show_favorite_control` as const;
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;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS =
`${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 =
`${CONF_TIMELINE}.controls.thumbnails.show_favorite_control` as const;
export const CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
`${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_POSITION = `${CONF_MENU}.position` as const;
export const CONF_MENU_STYLE = `${CONF_MENU}.style` as const;
+33 -16
View File
@@ -12,7 +12,6 @@ import {
upgradeConfig,
} from './config-mgmt.js';
import {
CONF_CAMERAS,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS,
@@ -21,9 +20,13 @@ import {
CONF_CAMERAS_ARRAY_FRIGATE_LABELS,
CONF_CAMERAS_ARRAY_FRIGATE_URL,
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_ID,
CONF_CAMERAS_ARRAY_IMAGE_REFRESH_SECONDS,
CONF_CAMERAS_ARRAY_IMAGE_URL,
CONF_CAMERAS_ARRAY_LIVE_PROVIDER,
CONF_CAMERAS_ARRAY_TITLE,
CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES,
@@ -31,13 +34,9 @@ import {
CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY,
CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL,
CONF_DIMENSIONS_ASPECT_RATIO,
CONF_CAMERAS,
CONF_DIMENSIONS_ASPECT_RATIO_MODE,
CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE,
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_DIMENSIONS_ASPECT_RATIO,
CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y,
@@ -53,6 +52,7 @@ import {
CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SIZE,
@@ -72,15 +72,21 @@ import {
CONF_LIVE_PRELOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
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_PAUSE,
CONF_MEDIA_VIEWER_AUTO_PLAY,
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_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE,
@@ -96,19 +102,22 @@ import {
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
CONF_MEDIA_VIEWER_LAZY_LOAD,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_BUTTONS,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_PROFILE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE,
@@ -118,29 +127,24 @@ import {
CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT,
CONF_VIEW_SCAN,
CONF_VIEW_SCAN_ENABLED,
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
CONF_VIEW_SCAN_UNTRIGGER_RESET,
CONF_VIEW_SCAN_UNTRIGGER_SECONDS,
CONF_VIEW_SCAN,
CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS,
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
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';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
import {
BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
FrigateCardConfig,
frigateCardConfigDefaults,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
@@ -1072,10 +1076,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathShowDetails: string,
configPathShowFavoriteControl: string,
configPathShowTimelineControl: string,
configPathShowDownloadControl: string,
defaults: {
show_details: boolean;
show_favorite_control: boolean;
show_timeline_control: boolean;
show_download_control: boolean;
},
options?: {
configPathMedia?: string;
@@ -1128,6 +1134,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
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_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.live.controls.thumbnails,
{
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_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.media_gallery.controls.thumbnails,
)}
${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_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.media_viewer.controls.thumbnails,
{
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_FAVORITE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
this._defaults.timeline.controls.thumbnails,
{
configPathMode: CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
+4 -2
View File
@@ -148,6 +148,7 @@
"right": "Thumbnails in a drawer to the right"
},
"show_details": "Show details with thumbnails",
"show_download_control": "Show download control on thumbnails",
"show_favorite_control": "Show favorite control on thumbnails",
"show_timeline_control": "Show timeline control on thumbnails",
"size": "Thumbnails size in pixels"
@@ -440,9 +441,10 @@
"start": "Start"
},
"thumbnail": {
"download": "Download media",
"no_thumbnail": "No thumbnail available",
"retain_indefinitely": "Event will be indefinitely retained",
"timeline": "See event/recording in timeline"
"retain_indefinitely": "Media will be indefinitely retained",
"timeline": "See media in timeline"
},
"timeline": {
"lock": "Lock timeline to a single event",
+1
View File
@@ -148,6 +148,7 @@
"right": "Miniature in un cassetto a destra"
},
"show_details": "",
"show_download_control": "",
"show_favorite_control": "Mostra il controllo preferito sulle miniature",
"show_timeline_control": "Mostra il controllo della sequenza temporale sulle miniature",
"size": "Dimensione delle miniature in pixel"
+1
View File
@@ -148,6 +148,7 @@
"right": "Miniaturas em uma gaveta à direita"
},
"show_details": "",
"show_download_control": "",
"show_favorite_control": "Mostrar controle de favorito nas miniaturas",
"show_timeline_control": "Mostrar controle da linha do tempo nas miniaturas",
"size": "Tamanho das miniaturas em pixels"
+14 -6
View File
@@ -6,12 +6,12 @@ import {
} from './types';
import { getConfigValue, setConfigValue } from './config-mgmt.js';
import {
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
CONF_LIVE_AUTO_MUTE,
CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
@@ -20,18 +20,23 @@ import {
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
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_PAUSE,
CONF_MEDIA_VIEWER_AUTO_PLAY,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_BUTTONS_FRIGATE,
CONF_MENU_BUTTONS_MEDIA_PLAYER,
@@ -43,11 +48,10 @@ import {
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
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_TIMELINE_CONTROL,
CONF_TIMELINE_SHOW_RECORDINGS,
CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
} from './const.js';
// 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,
// 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_TIMELINE_CONTROL]: 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_TIMELINE_CONTROL]: 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_TIMELINE_CONTROL]: 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_TIMELINE_CONTROL]: false,
[CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
+7 -1
View File
@@ -33,7 +33,6 @@ ha-icon {
border-radius: 50%;
opacity: 0.5;
color: var(--primary-color);
top: 3px;
cursor: pointer;
@@ -44,6 +43,7 @@ ha-icon:hover {
}
ha-icon.star {
top: 3px;
left: 3px;
}
ha-icon.star.starred {
@@ -51,9 +51,15 @@ ha-icon.star.starred {
}
ha-icon.timeline {
top: 3px;
right: 3px;
}
ha-icon.download {
right: 3px;
bottom: 3px;
}
frigate-card-thumbnail-details-event, frigate-card-thumbnail-details-recording {
flex: 1;
}
+6 -2
View File
@@ -245,7 +245,7 @@ const frigateCardLiveDependencySelectActionSchema =
frigate_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
const frigateCarMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({
const frigateCardMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('media_player'),
media_player: z.string(),
media_player_action: z.enum(['play', 'stop']),
@@ -255,7 +255,7 @@ export const frigateCardCustomActionSchema = z.union([
frigateCardGeneralActionSchema,
frigateCardCameraSelectActionSchema,
frigateCardLiveDependencySelectActionSchema,
frigateCarMediaPlayerActionSchema,
frigateCardMediaPlayerActionSchema,
]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -747,6 +747,7 @@ const thumbnailControlsDefaults = {
show_details: true,
show_favorite_control: true,
show_timeline_control: true,
show_download_control: true,
};
const thumbnailsControlSchema = z.object({
@@ -765,6 +766,9 @@ const thumbnailsControlSchema = z.object({
show_timeline_control: z
.boolean()
.default(thumbnailControlsDefaults.show_timeline_control),
show_download_control: z
.boolean()
.default(thumbnailControlsDefaults.show_download_control),
});
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();
}
};