- ${endTime
- ? getDurationString(startTime, endTime)
- : localize('event.in_progress')}
+
-
- ${format(this.seek, 'HH:mm:ss')}
+ ${seek
+ ? html`
+
+ ${seek}
`
: html``}
${eventCount !== null
- ? html`
-
- ${eventCount}
+ ? html`
+
+ ${eventCount}
`
: ``}
@@ -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`
`
- : ViewMediaClassifier.isRecording(this.media)
- ? html`
`
- : html``}
- ${shouldShowFavoriteControl
- ? html`
`
+ : ViewMediaClassifier.isRecording(this.media)
+ ? html``
+ : html``}
+ ${shouldShowFavoriteControl
+ ? html` `
- : ``}
- ${this.details && ViewMediaClassifier.isEvent(this.media)
- ? html`
`
- : this.details && ViewMediaClassifier.isRecording(this.media)
- ? html`
`
- : html``}
- ${shouldShowTimelineControl
- ? html`
{
- 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);
- }}
- >`
- : ''}`;
+ : ``}
+ ${this.details && ViewMediaClassifier.isEvent(this.media)
+ ? html`
`
+ : this.details && ViewMediaClassifier.isRecording(this.media)
+ ? html`
`
+ : html``}
+ ${shouldShowTimelineControl
+ ? html`
{
+ 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);
+ }}
+ >`
+ : ''}
+ ${shouldShowDownloadControl
+ ? html`
{
+ stopEventFromActivatingCardWideActions(ev);
+ if (this.hass && this.cameraManager && this.media) {
+ try {
+ await downloadMedia(this.hass, this.cameraManager, this.media);
+ } catch (error: unknown) {
+ dispatchFrigateCardErrorEvent(this, error);
+ }
+ }
+ }}
+ >`
+ : ``}
+ `;
}
/**
diff --git a/src/const.ts b/src/const.ts
index 7fbf58dc..4144e69e 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -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;
diff --git a/src/editor.ts b/src/editor.ts
index a874bc06..c5208104 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -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,
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 24653d4c..12931232 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -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",
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index 31679192..94040d26 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -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"
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 4cf9f0ec..9a788ea8 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -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"
diff --git a/src/performance.ts b/src/performance.ts
index 4467ff2b..62e54dfb 100644
--- a/src/performance.ts
+++ b/src/performance.ts
@@ -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,
diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss
index 9dbf577a..23c56eff 100644
--- a/src/scss/thumbnail.scss
+++ b/src/scss/thumbnail.scss
@@ -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;
}
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
index 231913ab..6592ed4e 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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
;
@@ -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;
diff --git a/src/utils/download.ts b/src/utils/download.ts
new file mode 100644
index 00000000..fe93acd9
--- /dev/null
+++ b/src/utils/download.ts
@@ -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 => {
+ 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();
+ }
+};