Add download control to thumbnails.
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export type CameraManagerCapabilities = BaseCapabilities;
|
||||
export type CameraManagerCameraCapabilities = BaseCapabilities;
|
||||
export interface CameraManagerMediaCapabilities {
|
||||
canFavorite: boolean;
|
||||
canDownload: boolean;
|
||||
}
|
||||
|
||||
export interface CameraManagerCameraMetadata {
|
||||
|
||||
+30
-75
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user