= createRef();
// Mapping of slide # to FrigateBrowseMediaSource child #.
@@ -645,7 +653,9 @@ export class FrigateCardViewerCarousel extends LitElement {
// If lazy loading is not enabled, wait for the media resolver task to
// complete and show a progress indictator until this.
if (!this.viewerConfig?.lazy_load && !this._isMediaFullyResolved()) {
- return renderTask(this, this._mediaResolutionTask, this._render.bind(this));
+ return renderTask(this, this._mediaResolutionTask, this._render.bind(this), {
+ cardWideConfig: this.cardWideConfig,
+ });
}
return this._render();
}
diff --git a/src/config-mgmt.ts b/src/config-mgmt.ts
index f95ce8ce..9cebd3cb 100644
--- a/src/config-mgmt.ts
+++ b/src/config-mgmt.ts
@@ -1,4 +1,6 @@
-import { cloneDeep, get, isEqual, set } from 'lodash-es';
+import get from 'lodash-es/get';
+import isEqual from 'lodash-es/isEqual';
+import set from 'lodash-es/set';
import {
CONF_CAMERAS,
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
@@ -40,7 +42,7 @@ import {
/**
* Set a configuration value.
* @param obj The configuration.
- * @param key The key to the property to set.
+ * @param keys The key to the property to set.
* @param value The value to set.
*/
@@ -55,7 +57,8 @@ export const setConfigValue = (
/**
* Get a configuration value.
* @param obj The configuration.
- * @param key The key to the property to retrieve.
+ * @param keys The key to the property to retrieve.
+ * @param def Default if key not found.
* @returns The property or undefined if not found.
*/
export const getConfigValue = (
@@ -104,7 +107,7 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
* @returns `true` if the configuration is upgradeable.
*/
export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean {
- const newObj = JSON.parse(JSON.stringify(obj));
+ const newObj = structuredClone(obj);
return upgradeConfig(newObj);
};
@@ -136,7 +139,7 @@ export const trimConfig = function (obj: RawFrigateCardConfig): boolean {
* @returns A new deeply-copied configuration.
*/
export const copyConfig = function (obj: RawFrigateCardConfig): RawFrigateCardConfig {
- return cloneDeep(obj);
+ return structuredClone(obj);
};
/**
diff --git a/src/const.ts b/src/const.ts
index ac6b6b6d..5468ea2e 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -139,6 +139,8 @@ export const CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS =
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` as const;
+export const CONF_LIVE_IMAGE_REFRESH_SECONDS =
+ `${CONF_LIVE}.image.refresh_seconds` as const;
export const CONF_LIVE_LAYOUT_FIT = `${CONF_LIVE}.layout.fit` as const;
export const CONF_LIVE_LAYOUT_POSITION_X = `${CONF_LIVE}.layout.position.x` as const;
export const CONF_LIVE_LAYOUT_POSITION_Y = `${CONF_LIVE}.layout.position.y` as const;
@@ -192,7 +194,10 @@ export const CONF_MENU_BUTTONS_FRIGATE_UI = `${CONF_MENU}.buttons.frigate_ui` as
export const CONF_MENU_BUTTONS_FULLSCREEN = `${CONF_MENU}.buttons.fullscreen` as const;
export const CONF_MENU_BUTTONS_IMAGE = `${CONF_MENU}.buttons.image` as const;
export const CONF_MENU_BUTTONS_LIVE = `${CONF_MENU}.buttons.live` as const;
+export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
+ `${CONF_MENU}.buttons.media_player` as const;
export const CONF_MENU_BUTTONS_SNAPSHOTS = `${CONF_MENU}.buttons.snapshots` as const;
+export const CONF_MENU_BUTTONS_TIMELINE = `${CONF_MENU}.buttons.timeline` as const;
export const CONF_DIMENSIONS = 'dimensions' as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
@@ -201,5 +206,11 @@ export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
export const CONF_OVERRIDES = 'overrides' as const;
+export const CONF_PERFORMANCE = 'performance' as const;
+export const CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR = `${CONF_PERFORMANCE}.features.animated_progress_indicator`;
+export const CONF_PERFORMANCE_PROFILE = `${CONF_PERFORMANCE}.profile`;
+export const CONF_PERFORMANCE_STYLE_BOX_SHADOW = `${CONF_PERFORMANCE}.style.box_shadow`;
+export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.border_radius`;
+
// Taken from https://github.dev/home-assistant/frontend/blob/b5861869e39290fd2e15737e89571dfc543b3ad3/src/data/media-player.ts#L93
export const MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA = 131072;
diff --git a/src/editor.ts b/src/editor.ts
index 6d6b58f5..fd20fb9c 100644
--- a/src/editor.ts
+++ b/src/editor.ts
@@ -62,6 +62,7 @@ import {
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
CONF_LIVE_CONTROLS_TITLE_MODE,
CONF_LIVE_DRAGGABLE,
+ CONF_LIVE_IMAGE_REFRESH_SECONDS,
CONF_LIVE_LAYOUT_FIT,
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
@@ -99,6 +100,10 @@ import {
CONF_MENU_BUTTON_SIZE,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
+ CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
+ 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,
@@ -125,6 +130,7 @@ import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
import {
BUTTON_SIZE_MIN,
+ FrigateCardConfig,
frigateCardConfigDefaults,
FRIGATE_MENU_PRIORITY_MAX,
RawFrigateCardConfig,
@@ -136,6 +142,7 @@ import { arrayMove } from './utils/basic.js';
import { getCameraID, getCameraTitle } from './utils/camera.js';
import { FRIGATE_ICON_SVG_PATH } from './utils/frigate.js';
import { getEntitiesFromHASS, sideLoadHomeAssistantElements } from './utils/ha';
+import { setLowPerformanceProfile } from './performance.js';
const MENU_BUTTONS = 'buttons';
const MENU_CAMERAS = 'cameras';
@@ -150,6 +157,7 @@ const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
+const MENU_LIVE_IMAGE = 'live.image';
const MENU_LIVE_LAYOUT = 'live.layout';
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
@@ -159,6 +167,8 @@ const MENU_MEDIA_VIEWER_CONTROLS_TITLE = 'media_viewer.controls.title';
const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
const MENU_OPTIONS = 'options';
+const MENU_PERFORMANCE_FEATURES = 'performance.features';
+const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_VIEW_SCAN = 'scan';
interface EditorOptionsSet {
@@ -226,6 +236,11 @@ const options: EditorOptions = {
name: localize('editor.dimensions'),
secondary: localize('editor.dimensions_secondary'),
},
+ performance: {
+ icon: 'speedometer',
+ name: localize('editor.performance'),
+ secondary: localize('editor.performance_secondary'),
+ },
overrides: {
icon: 'file-replace',
name: localize('editor.overrides'),
@@ -237,6 +252,8 @@ const options: EditorOptions = {
export class FrigateCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() protected _config?: RawFrigateCardConfig;
+ @state() protected _defaults = structuredClone(frigateCardConfigDefaults);
+
protected _initialized = false;
protected _configUpgradeable = false;
@@ -300,6 +317,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
value: 'none',
label: localize('config.common.controls.next_previous.styles.none'),
},
+ {
+ value: 'thumbnails',
+ label: localize('config.common.controls.next_previous.styles.thumbnails'),
+ },
];
protected _aspectRatioModes: EditorSelectOption[] = [
@@ -438,6 +459,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'below', label: localize('config.common.controls.timeline.modes.below') },
];
+ protected _performanceProfiles: EditorSelectOption[] = [
+ { value: '', label: '' },
+ { value: 'low', label: localize('config.performance.profiles.low') },
+ { value: 'high', label: localize('config.performance.profiles.high') },
+ ];
+
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -445,6 +472,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
// such, RawFrigateCardConfig is used as the type.
this._config = config;
this._configUpgradeable = isConfigUpgradeable(config);
+
+ let unvalidatedProfile: string | null = null;
+ try {
+ // this._config may not be a valid FrigateCardConfig as it has not been
+ // parsed. Attempt to pull out the performance profile.
+ unvalidatedProfile = (this._config as FrigateCardConfig).performance?.profile;
+ } catch (_) {}
+
+ if (unvalidatedProfile === 'high' || unvalidatedProfile === 'low') {
+ const defaults = structuredClone(frigateCardConfigDefaults);
+ if (unvalidatedProfile === 'low') {
+ setLowPerformanceProfile(this._config, defaults);
+ }
+ this._defaults = defaults;
+ }
}
/**
@@ -465,7 +507,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
* @param optionSetName The name of the EditorOptionsSet.
* @returns A rendered template.
*/
- protected _renderOptionSetHeader(optionSetName: string): TemplateResult {
+ protected _renderOptionSetHeader(
+ optionSetName: string,
+ titleClass?: string,
+ ): TemplateResult {
const optionSet = options[optionSetName];
return html`
@@ -477,7 +522,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
>
-
${optionSet.name}
+
${optionSet.name}
${optionSet.secondary}
@@ -669,24 +714,24 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html`
${this._renderSwitch(
CONF_VIEW_SCAN_ENABLED,
- frigateCardConfigDefaults.view.scan.enabled,
+ this._defaults.view.scan.enabled,
{
label: localize(`config.${CONF_VIEW_SCAN_ENABLED}`),
},
)}
${this._renderSwitch(
CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS,
- frigateCardConfigDefaults.view.scan.show_trigger_status,
+ this._defaults.view.scan.show_trigger_status,
{
label: localize(`config.${CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS}`),
},
)}
${this._renderSwitch(
CONF_VIEW_SCAN_UNTRIGGER_RESET,
- frigateCardConfigDefaults.view.scan.untrigger_reset,
+ this._defaults.view.scan.untrigger_reset,
)}
${this._renderNumberInput(CONF_VIEW_SCAN_UNTRIGGER_SECONDS, {
- default: frigateCardConfigDefaults.view.scan.untrigger_seconds,
+ default: this._defaults.view.scan.untrigger_seconds,
})}
`
: ''}
@@ -730,7 +775,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html`
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.enabled`,
- frigateCardConfigDefaults.menu.buttons[button]?.enabled ?? true,
+ this._defaults.menu.buttons[button]?.enabled ?? true,
{
label: localize('config.menu.buttons.enabled'),
},
@@ -744,7 +789,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
max: FRIGATE_MENU_PRIORITY_MAX,
- default: frigateCardConfigDefaults.menu.buttons[button]?.priority,
+ default: this._defaults.menu.buttons[button]?.priority,
label: localize('config.menu.buttons.priority'),
})}
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
@@ -882,6 +927,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathClusteringThreshold: string,
configPathTimelineMedia: string,
configPathShowRecordings: string,
+ showRecordingsDefault: boolean,
): TemplateResult | void {
return this._putInSubmenu(
domain,
@@ -896,7 +942,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathClusteringThreshold,
configPathTimelineMedia,
configPathShowRecordings,
- frigateCardConfigDefaults.mini_timeline.show_recordings,
+ showRecordingsDefault,
)}`,
);
}
@@ -912,6 +958,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
domain: string,
configPathStyle: string,
configPathSize: string,
+ options?: {
+ allowIcons?: boolean;
+ allowThumbnails?: boolean;
+ },
): TemplateResult | void {
return this._putInSubmenu(
domain,
@@ -919,9 +969,17 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
'config.common.controls.next_previous.editor_label',
{ name: 'mdi:arrow-right-bold-circle' },
html`
- ${this._renderOptionSelector(configPathStyle, this._nextPreviousControlStyles, {
- label: localize('config.common.controls.next_previous.style'),
- })}
+ ${this._renderOptionSelector(
+ configPathStyle,
+ this._nextPreviousControlStyles.filter(
+ (item) =>
+ (!!options?.allowThumbnails || item.value !== 'thumbnails') &&
+ (!!options?.allowIcons || item.value !== 'icons'),
+ ),
+ {
+ label: localize('config.common.controls.next_previous.style'),
+ },
+ )}
${this._renderNumberInput(configPathSize, {
min: BUTTON_SIZE_MIN,
label: localize('config.common.controls.next_previous.size'),
@@ -983,21 +1041,21 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
})}
${this._renderSwitch(
configPathShowDetails,
- frigateCardConfigDefaults.live.controls.thumbnails.show_details,
+ this._defaults.live.controls.thumbnails.show_details,
{
label: localize('config.common.controls.thumbnails.show_details'),
},
)}
${this._renderSwitch(
configPathShowFavoriteControl,
- frigateCardConfigDefaults.live.controls.thumbnails.show_favorite_control,
+ this._defaults.live.controls.thumbnails.show_favorite_control,
{
label: localize('config.common.controls.thumbnails.show_favorite_control'),
},
)}
${this._renderSwitch(
configPathShowTimelineControl,
- frigateCardConfigDefaults.live.controls.thumbnails.show_timeline_control,
+ this._defaults.live.controls.thumbnails.show_timeline_control,
{
label: localize('config.common.controls.thumbnails.show_timeline_control'),
},
@@ -1051,6 +1109,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: '', label: '' },
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
+ {
+ value: 'image',
+ label: localize('config.cameras.live_providers.image'),
+ },
{
value: 'frigate-jsmpeg',
label: localize('config.cameras.live_providers.frigate-jsmpeg'),
@@ -1231,7 +1293,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS,
cameraIndex,
),
- frigateCardConfigDefaults.cameras.dependencies.all_cameras,
+ this._defaults.cameras.dependencies.all_cameras,
)}
${this._renderOptionSelector(
getArrayConfigPath(
@@ -1251,11 +1313,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ name: 'mdi:magnify-scan' },
html` ${this._renderSwitch(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, cameraIndex),
- frigateCardConfigDefaults.cameras.triggers.occupancy,
+ this._defaults.cameras.triggers.occupancy,
)}
${this._renderSwitch(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_MOTION, cameraIndex),
- frigateCardConfigDefaults.cameras.triggers.motion,
+ this._defaults.cameras.triggers.motion,
)}
${this._renderOptionSelector(
getArrayConfigPath(CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES, cameraIndex),
@@ -1365,7 +1427,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
return html``;
}
- const defaults = frigateCardConfigDefaults;
const entities = getEntitiesFromHASS(this.hass);
const cameras = (getConfigValue(this._config, CONF_CAMERAS) ||
[]) as RawFrigateCardConfigArray;
@@ -1415,10 +1476,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
- ${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
+ ${this._renderSwitch(
+ CONF_VIEW_UPDATE_FORCE,
+ this._defaults.view.update_force,
+ )}
${this._renderSwitch(
CONF_VIEW_UPDATE_CYCLE_CAMERA,
- defaults.view.update_cycle_camera,
+ this._defaults.view.update_cycle_camera,
)}
${this._renderViewScanMenu()}
@@ -1453,9 +1517,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._expandedMenus[MENU_OPTIONS] === 'live'
? html`
- ${this._renderSwitch(CONF_LIVE_PRELOAD, defaults.live.preload)}
- ${this._renderSwitch(CONF_LIVE_DRAGGABLE, defaults.live.draggable)}
- ${this._renderSwitch(CONF_LIVE_LAZY_LOAD, defaults.live.lazy_load)}
+ ${this._renderSwitch(CONF_LIVE_PRELOAD, this._defaults.live.preload)}
+ ${this._renderSwitch(CONF_LIVE_DRAGGABLE, this._defaults.live.draggable)}
+ ${this._renderSwitch(CONF_LIVE_LAZY_LOAD, this._defaults.live.lazy_load)}
${this._renderOptionSelector(
CONF_LIVE_LAZY_UNLOAD,
this._mediaActionNegativeConditions,
@@ -1482,7 +1546,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
${this._renderSwitch(
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
- defaults.live.show_image_during_load,
+ this._defaults.live.show_image_during_load,
)}
${this._putInSubmenu(
MENU_LIVE_CONTROLS,
@@ -1494,6 +1558,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
+ {
+ allowIcons: true,
+ },
)}
${this._renderThumbnailsControls(
MENU_LIVE_CONTROLS_THUMBNAILS,
@@ -1518,6 +1585,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_LIVE_CONTROLS_TIMELINE_MEDIA,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
+ this._defaults.live.controls.timeline.show_recordings,
)}
`,
)}
@@ -1528,6 +1596,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
)}
+ ${this._putInSubmenu(
+ MENU_LIVE_IMAGE,
+ true,
+ 'config.live.image.editor_label',
+ { name: 'mdi:image-sync-outline' },
+ html` ${this._renderNumberInput(CONF_LIVE_IMAGE_REFRESH_SECONDS)} `,
+ )}
`
: ''}
@@ -1564,11 +1639,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_DRAGGABLE,
- defaults.media_viewer.draggable,
+ this._defaults.media_viewer.draggable,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_LAZY_LOAD,
- defaults.media_viewer.lazy_load,
+ this._defaults.media_viewer.lazy_load,
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
@@ -1584,6 +1659,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
+ {
+ allowThumbnails: true,
+ },
)}
${this._renderThumbnailsControls(
MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS,
@@ -1607,6 +1685,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_CLUSTERING_THRESHOLD,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MEDIA,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
+ this._defaults.media_viewer.controls.timeline.show_recordings,
)}
`,
)}
@@ -1642,7 +1721,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_MEDIA,
CONF_TIMELINE_SHOW_RECORDINGS,
- defaults.timeline.show_recordings,
+ this._defaults.timeline.show_recordings,
)}
${this._renderThumbnailsControls(
MENU_TIMELINE_CONTROLS_THUMBNAILS,
@@ -1666,6 +1745,51 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderStringInput(CONF_DIMENSIONS_ASPECT_RATIO)}
`
: ''}
+ ${this._renderOptionSetHeader(
+ 'performance',
+ getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
+ ? 'warning'
+ : undefined,
+ )}
+ ${this._expandedMenus[MENU_OPTIONS] === 'performance'
+ ? html`
+ ${getConfigValue(this._config, CONF_PERFORMANCE_PROFILE) === 'low'
+ ? this._renderInfo(localize('config.performance.warning'))
+ : html``}
+ ${this._renderOptionSelector(
+ CONF_PERFORMANCE_PROFILE,
+ this._performanceProfiles,
+ )}
+ ${this._putInSubmenu(
+ MENU_PERFORMANCE_FEATURES,
+ true,
+ 'config.performance.features.editor_label',
+ { name: 'mdi:feature-search' },
+ html`
+ ${this._renderSwitch(
+ CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
+ this._defaults.performance.features.animated_progress_indicator,
+ )}
+ `,
+ )}
+ ${this._putInSubmenu(
+ MENU_PERFORMANCE_STYLE,
+ true,
+ 'config.performance.style.editor_label',
+ { name: 'mdi:palette-swatch-variant' },
+ html`
+ ${this._renderSwitch(
+ CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
+ this._defaults.performance.style.border_radius,
+ )}
+ ${this._renderSwitch(
+ CONF_PERFORMANCE_STYLE_BOX_SHADOW,
+ this._defaults.performance.style.box_shadow,
+ )}
+ `,
+ )}
+
`
+ : ''}
${this._config['overrides'] !== undefined
? html` ${this._renderOptionSetHeader('overrides')}
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index 7c2f0b57..b8b16753 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -33,7 +33,8 @@
"live_providers": {
"auto": "Automatic",
"frigate-jsmpeg": "Frigate JSMpeg",
- "ha": "Home Assistant (i.e. HLS, LL-HLS, WebRTC native)",
+ "ha": "Home Assistant video stream (i.e. HLS, LL-HLS, WebRTC native)",
+ "image": "Home Assistant images",
"webrtc-card": "WebRTC Card (i.e. AlexxIT's WebRTC Card)"
},
"title": "Title for this camera (Autodetected from entity)",
@@ -89,7 +90,8 @@
"styles": {
"chevrons": "Chevrons",
"icons": "Icons",
- "none": "None"
+ "none": "None",
+ "thumbnails": "Thumbnails"
}
},
"timeline": {
@@ -164,6 +166,10 @@
"editor_label": "Live Controls"
},
"draggable": "Live cameras view can be dragged/swiped",
+ "image": {
+ "editor_label": "Image Live Provider Options",
+ "refresh_seconds": "Number of seconds after which to refresh live image (0=never)"
+ },
"layout": "Live Layout",
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
@@ -238,6 +244,23 @@
"overrides": {
"info": "This card configuration has manually specified overrides configured which may override values shown in the visual editor, please consult the code editor to view/modify these overrides"
},
+ "performance": {
+ "warning": "This card is in low profile mode so defaults have changed to optimize performance",
+ "features": {
+ "editor_label": "Feature Options",
+ "animated_progress_indicator": "Animated Progress Indicator"
+ },
+ "profile": "Performance profile",
+ "profiles": {
+ "low": "Low performance",
+ "high": "High/full performance"
+ },
+ "style": {
+ "editor_label": "Style Options",
+ "box_shadow": "Shadows",
+ "border_radius": "Curves"
+ }
+ },
"view": {
"camera_select": "View for newly selected cameras",
"dark_mode": "Dark mode",
@@ -295,6 +318,8 @@
"move_up": "Move up",
"overrides": "Overrides are active",
"overrides_secondary": "Dynamic configuration overrides detected",
+ "performance": "Performance",
+ "performance_secondary": "Card performance options",
"timeline": "Timeline",
"timeline_secondary": "Event timeline options",
"upgrade": "Upgrade",
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index 4b652897..8364175e 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -88,7 +88,8 @@
"styles": {
"chevrons": "Chevrons",
"icons": "Icone",
- "none": "Icone"
+ "none": "Nessuno",
+ "thumbnails": "Miniature"
}
},
"timeline": {
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 92b3a975..f93707a5 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -88,7 +88,8 @@
"styles": {
"chevrons": "Setas",
"icons": "Ícones",
- "none": "Nenhum"
+ "none": "Nenhum",
+ "thumbnails": "Miniaturas"
}
},
"timeline": {
diff --git a/src/localize/localize.ts b/src/localize/localize.ts
index cf174c5e..e74cff88 100644
--- a/src/localize/localize.ts
+++ b/src/localize/localize.ts
@@ -1,14 +1,16 @@
import * as en from './languages/en.json';
-import * as pt_BR from './languages/pt-BR.json';
-import * as it from './languages/it.json';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const languages: Record = {
+ // English as always loaded as it's the fallback language that will be used
+ // when translations are not found or before they are loaded (via
+ // loadLanguages()).
en: en,
- pt_BR: pt_BR,
- it: it,
-};
+}
+/**
+ * Get the configured language.
+ */
export function getLanguage(): string {
const canonicalizeLanguage = (language?: string | null): string | null => {
if (!language) {
@@ -39,14 +41,32 @@ export function getLanguage(): string {
return lang || 'en';
}
+/**
+ * Load required languages.
+ */
+export const loadLanguages = async (): Promise => {
+ const lang = getLanguage();
+ if (lang === 'it') {
+ languages['it'] = await import('./languages/it.json');
+ } else if (lang === 'pt_BR') {
+ languages['pt_BR'] = await import('./languages/pt-BR.json');
+ }
+}
+
+/**
+ * Get a localized version of a given string key.
+ * @param string The key.
+ * @param search An optional search key to be used with 'replace'.
+ * @param replace An optional replacement text to be used with 'search'.
+ * @returns
+ */
export function localize(string: string, search = '', replace = ''): string {
const lang = getLanguage();
- let translated: string;
+ let translated = '';
try {
translated = string.split('.').reduce((o, i) => o[i], languages[lang]);
- } catch (e) {
- translated = string.split('.').reduce((o, i) => o[i], languages['en']);
+ } catch (_) {
}
if (!translated) {
diff --git a/src/performance.ts b/src/performance.ts
new file mode 100644
index 00000000..d59e5132
--- /dev/null
+++ b/src/performance.ts
@@ -0,0 +1,198 @@
+import { deepRemoveDefaults } from './utils/zod.js';
+import {
+ frigateCardConfigSchema,
+ RawFrigateCardConfig,
+ PerformanceConfig,
+} from './types';
+import { getArrayConfigPath, getConfigValue, setConfigValue } from './config-mgmt.js';
+import {
+ CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY,
+ CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS,
+ CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
+ CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
+ CONF_LIVE_AUTO_MUTE,
+ CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
+ CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
+ CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
+ CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
+ CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
+ CONF_LIVE_CONTROLS_TITLE_MODE,
+ CONF_LIVE_DRAGGABLE,
+ CONF_LIVE_IMAGE_REFRESH_SECONDS,
+ CONF_LIVE_LAZY_UNLOAD,
+ CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
+ CONF_LIVE_TRANSITION_EFFECT,
+ CONF_MEDIA_VIEWER_AUTO_MUTE,
+ CONF_MEDIA_VIEWER_AUTO_PAUSE,
+ CONF_MEDIA_VIEWER_AUTO_PLAY,
+ 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_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_TRANSITION_EFFECT,
+ CONF_MENU_BUTTONS_FRIGATE,
+ CONF_MENU_BUTTONS_MEDIA_PLAYER,
+ CONF_MENU_BUTTONS_TIMELINE,
+ CONF_MENU_STYLE,
+ CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
+ CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
+ CONF_PERFORMANCE_STYLE_BOX_SHADOW,
+ CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
+ CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
+ CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
+ CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
+ CONF_TIMELINE_SHOW_RECORDINGS,
+} from './const.js';
+
+// Caution: These values are applied after parsing (since we cannot know the
+// performance profile until afterwards), so there is no validation on these
+// defaults.
+const LOW_PROFILE_DEFAULTS = {
+ // Disable thumbnail carousels.
+ [CONF_LIVE_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
+ [CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
+ [CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE]: 'none' as const,
+
+ // Do not show recordings on timelines.
+ [CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
+ [CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS]: false,
+ [CONF_TIMELINE_SHOW_RECORDINGS]: false,
+
+ // Take no automatic media actions.
+ [CONF_LIVE_AUTO_MUTE]: 'never' as const,
+ [CONF_MEDIA_VIEWER_AUTO_PLAY]: 'never' as const,
+ [CONF_MEDIA_VIEWER_AUTO_PAUSE]: 'never' as const,
+ [CONF_MEDIA_VIEWER_AUTO_MUTE]: 'never' as const,
+
+ // Always unload resources that are lazily loaded.
+ [CONF_LIVE_LAZY_UNLOAD]: 'all' as const,
+
+ // Media carousels do not drag.
+ [CONF_LIVE_DRAGGABLE]: false,
+ [CONF_MEDIA_VIEWER_DRAGGABLE]: false,
+
+ // Media carousels have no effects.
+ [CONF_LIVE_TRANSITION_EFFECT]: 'none' as const,
+ [CONF_MEDIA_VIEWER_TRANSITION_EFFECT]: 'none' as const,
+
+ // Do not show image during load.
+ [CONF_LIVE_SHOW_IMAGE_DURING_LOAD]: false,
+
+ // Media player next/previous are chevrons.
+ [CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE]: 'chevrons' as const,
+
+ [CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE]: 'none' as const,
+ [CONF_LIVE_CONTROLS_TITLE_MODE]: 'none' as const,
+
+ // Move the menu to outside to remove the need to interact with it with open.
+ [CONF_MENU_STYLE]: 'outside',
+
+ // Hide several buttons that are otherwise visible by default.
+ [`${CONF_MENU_BUTTONS_FRIGATE}.enabled`]: false,
+ [`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
+ [`${CONF_MENU_BUTTONS_MEDIA_PLAYER}.enabled`]: false,
+ [`${CONF_MENU_BUTTONS_TIMELINE}.enabled`]: false,
+
+ // Disable all options in thumbnails.
+ [CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
+ [CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
+ [CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
+ [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL]: false,
+ [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
+ [CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
+
+ // Refresh the live camera image every 10 seconds (same as stock Home
+ // Assistant Picture Glance).
+ [CONF_LIVE_IMAGE_REFRESH_SECONDS]: 10,
+
+ [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_FAVORITE_CONTROL]: false,
+ [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL]: false,
+ [CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS]: false,
+
+ // Disable all optional performance related features.
+ [CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR]: false,
+
+ // Disable all expensive CSS features.
+ [CONF_PERFORMANCE_STYLE_BORDER_RADIUS]: false,
+ [CONF_PERFORMANCE_STYLE_BOX_SHADOW]: false,
+};
+
+const LOW_PROFILE_CAMERA_DEFAULTS = {
+ [CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY]: false,
+};
+
+/**
+ * Set low performance profile mode. Sets flags as defined in
+ * LOW_PROFILE_DEFAULTS unless they are explicitly overriden in the
+ * configuration.
+ * @param inputConfig The raw unparsed input configuration.
+ * @param outputConfig The output config to write to.
+ * @returns A changed (in-place) parsed input configuration.
+ */
+export const setLowPerformanceProfile = (
+ inputConfig: RawFrigateCardConfig,
+ outputConfig: T,
+): T => {
+ const setIfNotSpecified = (
+ defaultLessConfig: RawFrigateCardConfig,
+ outputConfig: T,
+ key: string,
+ value: unknown,
+ ) => {
+ if (getConfigValue(defaultLessConfig, key) === undefined) {
+ setConfigValue(outputConfig, key, value);
+ }
+ };
+
+ const defaultLessParseResult = deepRemoveDefaults(frigateCardConfigSchema).safeParse(
+ inputConfig,
+ );
+ if (defaultLessParseResult.success) {
+ const defaultLessConfig = defaultLessParseResult.data;
+ Object.entries(LOW_PROFILE_DEFAULTS).forEach(([k, v]: [string, unknown]) =>
+ setIfNotSpecified(defaultLessConfig, outputConfig, k, v),
+ );
+
+ Object.entries(LOW_PROFILE_CAMERA_DEFAULTS).forEach(
+ ([rawKey, v]: [string, unknown]) => {
+ defaultLessConfig.cameras.forEach((_, index: number) => {
+ const indexedKey = getArrayConfigPath(rawKey, index);
+ setIfNotSpecified(defaultLessConfig, outputConfig, indexedKey, v);
+ });
+ },
+ );
+ }
+ return outputConfig;
+};
+
+const STYLE_DISABLE_MAP = {
+ box_shadow: 'none',
+ border_radius: '0px',
+};
+
+/**
+ * Set card-wide CSS variables for performance.
+ * @param element The element to set the variables on.
+ * @param performance The performance configuration.
+ */
+export const setPerformanceCSSStyles = (
+ element: HTMLElement,
+ performance?: PerformanceConfig,
+): void => {
+ const styles = performance?.style ?? {};
+ for (const configKey of Object.keys(styles)) {
+ const CSSKey = `--frigate-card-css-${configKey.replaceAll('_', '-')}`;
+ if (styles[configKey] === false) {
+ element.style.setProperty(CSSKey, STYLE_DISABLE_MAP[configKey]);
+ } else {
+ element.style.removeProperty(CSSKey);
+ }
+ }
+};
diff --git a/src/scss/drawer-inject.scss b/src/scss/drawer-inject.scss
index 17eaeeef..bdbd8cf7 100644
--- a/src/scss/drawer-inject.scss
+++ b/src/scss/drawer-inject.scss
@@ -27,16 +27,16 @@
max-width: 90%;
}
-:host([location=right]) #d {
+:host([location='right']) #d {
// Position to the right.
left: unset;
right: 0;
transform: translateX(100%);
}
-:host([location=right][open]) #d {
+:host([location='right'][open]) #d {
transform: none;
- box-shadow: 0px 0px 25px 0px black;
+ box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 25px 0px black);
}
#ifs {
diff --git a/src/scss/editor.scss b/src/scss/editor.scss
index 2ddddce6..cbce8562 100644
--- a/src/scss/editor.scss
+++ b/src/scss/editor.scss
@@ -17,6 +17,9 @@
margin-top: -6px;
pointer-events: none;
}
+.title.warning {
+ color: var(--warning-color);
+}
.secondary {
padding-left: 40px;
color: var(--secondary-text-color);
@@ -101,7 +104,7 @@ div.upgrade span {
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
}
span.info {
- padding: 4px;
+ padding: 10px;
}
ha-selector {
diff --git a/src/scss/gallery.scss b/src/scss/gallery.scss
index 662c0a78..ad22eb9d 100644
--- a/src/scss/gallery.scss
+++ b/src/scss/gallery.scss
@@ -31,7 +31,7 @@
text-align: center;
color: var(--primary-text-color, white);
border: 1px solid var(--primary-color);
- border-radius: var(--ha-card-border-radius, 4px);
+ border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
// Folder background color should match the thumbnail element background
// color.
diff --git a/src/scss/message.scss b/src/scss/message.scss
index 50746fb2..953c9331 100644
--- a/src/scss/message.scss
+++ b/src/scss/message.scss
@@ -54,6 +54,10 @@ div.message div.icon {
word-break: break-all;
}
+.message ha-icon, ha-circular-progress {
+ padding: 10px;
+}
+
.dotdotdot:after {
@keyframes dots {
0%,
diff --git a/src/scss/next-previous-control.scss b/src/scss/next-previous-control.scss
index 2d9e97b0..178a77a3 100644
--- a/src/scss/next-previous-control.scss
+++ b/src/scss/next-previous-control.scss
@@ -29,7 +29,7 @@
border-radius: 50%;
height: var(--frigate-card-next-prev-size);
top: calc(50% - (var(--frigate-card-next-prev-size) / 2));
- box-shadow: 0px 0px 20px 5px black;
+ box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 20px 5px black);
transition: all 0.2s ease-out;
opacity: 0.8;
aspect-ratio: 1 / 1;
diff --git a/src/scss/thumbnail-feature-event.scss b/src/scss/thumbnail-feature-event.scss
index f4a1364e..970fb6b9 100644
--- a/src/scss/thumbnail-feature-event.scss
+++ b/src/scss/thumbnail-feature-event.scss
@@ -12,7 +12,7 @@ ha-icon {
// Safari will occasionally not load thumbnails correctly with display block.
display: inline-block;
- border-radius: var(--ha-card-border-radius, 4px);
+ border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
max-width: var(--frigate-card-thumbnail-size);
max-height: 100%;
diff --git a/src/scss/thumbnail-feature-recording.scss b/src/scss/thumbnail-feature-recording.scss
index dab70503..459e3e22 100644
--- a/src/scss/thumbnail-feature-recording.scss
+++ b/src/scss/thumbnail-feature-recording.scss
@@ -12,7 +12,7 @@
border: 1px solid var(--secondary-color);
background-color: var(--secondary-background-color);
- border-radius: var(--ha-card-border-radius, 4px);
+ border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
box-sizing: border-box;
color: var(--primary-text-color);
diff --git a/src/scss/thumbnail.scss b/src/scss/thumbnail.scss
index caa64791..9dbf577a 100644
--- a/src/scss/thumbnail.scss
+++ b/src/scss/thumbnail.scss
@@ -16,7 +16,7 @@
:host([details]) {
border: 1px solid var(--primary-color);
- border-radius: var(--ha-card-border-radius, 4px);
+ border-radius: var(--frigate-card-css-border-radius, var(--ha-card-border-radius, 4px));
padding: 2px;
// When details are enabled, use a background color so that the details have
diff --git a/src/scss/timeline-core.scss b/src/scss/timeline-core.scss
index a6851b2a..218d8b5c 100644
--- a/src/scss/timeline-core.scss
+++ b/src/scss/timeline-core.scss
@@ -49,7 +49,7 @@ div.timeline {
.vis-item.vis-selected {
border-color: var(--accent-color);
background-color: var(--accent-color);
- box-shadow: 0px 0px 5px 1px var(--primary-color);
+ box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
}
.vis-item.vis-background {
background-color: var(--primary-color);
@@ -88,7 +88,10 @@ div.timeline {
border-style: dotted;
color: var(--primary-text-color);
background-color: var(--primary-background-color);
- box-shadow: 0px 0px 5px 1px var(--primary-color);
+ box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
+}
+.vis-item.vis-range {
+ border-radius: var(--frigate-card-css-border-radius, unset);
}
.vis-time-axis .vis-grid.vis-minor {
@@ -127,7 +130,7 @@ div.vis-tooltip {
.target_bar {
border-left: 2px solid var(--primary-color);
opacity: 0.7;
- box-shadow: 0px 0px 3px 1px var(--primary-color);
+ box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 3px 1px var(--primary-color));
// Prevent the mouse interacting with the custom time.
pointer-events: none;
diff --git a/src/types.ts b/src/types.ts
index f6f51c37..aaf68b98 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -59,7 +59,7 @@ const FRIGATE_MENU_ALIGNMENTS = FRIGATE_MENU_POSITIONS;
export const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
-const LIVE_PROVIDERS = ['auto', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
+const LIVE_PROVIDERS = ['auto', 'image', 'ha', 'frigate-jsmpeg', 'webrtc-card'] as const;
export type LiveProvider = typeof LIVE_PROVIDERS[number];
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
@@ -206,7 +206,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
'menu_toggle',
'diagnostics',
'recording',
- 'recordings'
+ 'recordings',
] as const;
const FRIGATE_CARD_ACTIONS = [
...FRIGATE_CARD_GENERAL_ACTIONS,
@@ -696,8 +696,12 @@ const timelineCoreConfigSchema = z.object({
});
export type TimelineCoreConfig = z.infer;
+const miniTimelineConfigDefault = {
+ ...timelineCoreConfigDefault,
+ mode: 'none' as const,
+}
const miniTimelineConfigSchema = timelineCoreConfigSchema.extend({
- mode: z.enum(['none', 'above', 'below']),
+ mode: z.enum(['none', 'above', 'below']).default(miniTimelineConfigDefault.mode),
});
export type MiniTimelineControlConfig = z.infer;
@@ -735,6 +739,11 @@ export type TitleControlConfig = z.infer;
/**
* Live view configuration section.
*/
+
+const liveImageConfigDefault = {
+ refresh_seconds: 1,
+};
+
const liveConfigDefault = {
auto_play: 'all' as const,
auto_pause: 'never' as const,
@@ -746,6 +755,7 @@ const liveConfigDefault = {
draggable: true,
transition_effect: 'slide' as const,
show_image_during_load: true,
+ image: liveImageConfigDefault,
controls: {
next_previous: {
size: 48,
@@ -759,6 +769,7 @@ const liveConfigDefault = {
show_timeline_control: true,
mode: 'left' as const,
},
+ timeline: miniTimelineConfigDefault,
title: {
mode: 'popup-bottom-right' as const,
duration_seconds: 2,
@@ -766,6 +777,11 @@ const liveConfigDefault = {
},
};
+const liveImageConfigSchema = z.object({
+ refresh_seconds: z.number().min(0).default(liveConfigDefault.image.refresh_seconds),
+});
+export type LiveImageConfig = z.infer;
+
const webrtcCardConfigSchema = webrtcCardCameraConfigSchema.passthrough().optional();
export type WebRTCCardConfig = z.infer;
@@ -794,8 +810,9 @@ export type JSMPEGConfig = z.infer;
const liveOverridableConfigSchema = z
.object({
- webrtc_card: webrtcCardConfigSchema,
+ image: liveImageConfigSchema.default(liveConfigDefault.image),
jsmpeg: jsmpegConfigSchema,
+ webrtc_card: webrtcCardConfigSchema,
controls: z
.object({
next_previous: nextPreviousControlConfigSchema
@@ -833,7 +850,7 @@ const liveOverridableConfigSchema = z
.default(liveConfigDefault.controls.thumbnails.media),
})
.default(liveConfigDefault.controls.thumbnails),
- timeline: miniTimelineConfigSchema.optional(),
+ timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
title: titleControlConfigSchema
.extend({
mode: titleControlConfigSchema.shape.mode.default(
@@ -975,6 +992,7 @@ const viewerConfigDefault = {
show_timeline_control: true,
mode: 'left' as const,
},
+ timeline: miniTimelineConfigDefault,
title: {
mode: 'popup-bottom-right' as const,
duration_seconds: 2,
@@ -1038,7 +1056,7 @@ const viewerConfigSchema = z
),
})
.default(viewerConfigDefault.controls.thumbnails),
- timeline: miniTimelineConfigSchema.optional(),
+ timeline: miniTimelineConfigSchema.default(viewerConfigDefault.controls.timeline),
title: titleControlConfigSchema
.extend({
mode: titleControlConfigSchema.shape.mode.default(
@@ -1206,6 +1224,41 @@ const liveOverridesSchema = z
.optional();
export type LiveOverrides = z.infer;
+const performanceConfigDefault = {
+ profile: 'high' as const,
+ features: {
+ animated_progress_indicator: true,
+ },
+ style: {
+ border_radius: true,
+ box_shadow: true,
+ },
+};
+
+const performanceConfigSchema = z
+ .object({
+ profile: z.enum(['low', 'high']).default(performanceConfigDefault.profile),
+ features: z
+ .object({
+ animated_progress_indicator: z
+ .boolean()
+ .default(performanceConfigDefault.features.animated_progress_indicator),
+ })
+ .default(performanceConfigDefault.features),
+ style: z
+ .object({
+ border_radius: z.boolean().default(performanceConfigDefault.style.border_radius),
+ box_shadow: z.boolean().default(performanceConfigDefault.style.box_shadow),
+ })
+ .default(performanceConfigDefault.style),
+ })
+ .default(performanceConfigDefault);
+export type PerformanceConfig = z.infer;
+
+export interface CardWideConfig {
+ performance?: PerformanceConfig;
+}
+
/**
* Main card config.
*/
@@ -1221,6 +1274,7 @@ export const frigateCardConfigSchema = z.object({
elements: pictureElementsSchema,
dimensions: dimensionsConfigSchema,
timeline: timelineConfigSchema,
+ performance: performanceConfigSchema,
// Configuration overrides.
overrides: overridesSchema,
@@ -1245,7 +1299,7 @@ export const frigateCardConfigDefaults = {
event_gallery: galleryConfigDefault,
image: imageConfigDefault,
timeline: timelineConfigDefault,
- mini_timeline: timelineCoreConfigDefault,
+ performance: performanceConfigDefault,
};
const menuButtonSchema = z.discriminatedUnion('type', [
diff --git a/src/utils/basic.ts b/src/utils/basic.ts
index d44142fd..a9f93620 100644
--- a/src/utils/basic.ts
+++ b/src/utils/basic.ts
@@ -1,5 +1,5 @@
-import { format } from 'date-fns';
-import { isEqual } from 'lodash-es';
+import format from 'date-fns/format';
+import isEqual from 'lodash-es/isEqual';
import { FrigateCardError } from '../types';
/**
diff --git a/src/utils/data-manager.ts b/src/utils/data-manager.ts
index 3d251889..4eda65c9 100644
--- a/src/utils/data-manager.ts
+++ b/src/utils/data-manager.ts
@@ -1,6 +1,6 @@
import { HomeAssistant } from 'custom-card-helpers';
import { DataSet, DataView } from 'vis-data/esnext';
-import { IdType, TimelineItem } from 'vis-timeline/esnext';
+import type { IdType, TimelineItem } from 'vis-timeline/esnext';
import { CAMERA_BIRDSEYE } from '../const.js';
import {
CameraConfig,
@@ -20,7 +20,7 @@ import {
} from './frigate.js';
import { dispatchFrigateCardErrorEvent } from '../components/message.js';
import fromUnixTime from 'date-fns/fromUnixTime';
-import { throttle } from 'lodash-es';
+import throttle from 'lodash-es/throttle';
const RECORDING_SEGMENT_TOLERANCE = 60;
const DATA_MANAGER_MAX_AGE_SECONDS = 10;
@@ -104,7 +104,6 @@ export class DataManager {
protected _maxAgeSeconds: number = DATA_MANAGER_MAX_AGE_SECONDS;
protected _cameras: Map;
- protected _mediaType: TimelineMediaType;
// Garbage collect segments at most once an hour.
protected _throttledSegmentGarbageCollector = throttle(
@@ -115,9 +114,8 @@ export class DataManager {
{ trailing: true },
);
- constructor(cameras: Map, mediaType: TimelineMediaType) {
+ constructor(cameras: Map) {
this._cameras = cameras;
- this._mediaType = mediaType;
}
// Get the last event fetch date.
diff --git a/src/utils/ha/browse-media.ts b/src/utils/ha/browse-media.ts
index 8fce396d..ac72e964 100644
--- a/src/utils/ha/browse-media.ts
+++ b/src/utils/ha/browse-media.ts
@@ -1,4 +1,5 @@
import { HomeAssistant } from 'custom-card-helpers';
+import { ViewContext } from 'view';
import { homeAssistantWSRequest } from '.';
import {
dispatchErrorMessageEvent,
@@ -346,6 +347,7 @@ export const fetchChildMediaAndDispatchViewChange = async (
hass: HomeAssistant,
view: Readonly,
child: Readonly,
+ context?: ViewContext,
): Promise => {
let parent: FrigateBrowseMediaSource;
try {
@@ -358,6 +360,7 @@ export const fetchChildMediaAndDispatchViewChange = async (
.evolve({
target: parent,
})
+ .mergeInContext(context)
.dispatchChangeEvent(element);
};
diff --git a/src/utils/media-to-view.ts b/src/utils/media-to-view.ts
index a37cd098..c6bb6413 100644
--- a/src/utils/media-to-view.ts
+++ b/src/utils/media-to-view.ts
@@ -1,4 +1,9 @@
-import { add, endOfHour, fromUnixTime, getUnixTime, startOfHour, sub } from 'date-fns';
+import add from 'date-fns/add';
+import endOfHour from 'date-fns/endOfHour';
+import fromUnixTime from 'date-fns/fromUnixTime';
+import getUnixTime from 'date-fns/getUnixTime';
+import startOfHour from 'date-fns/startOfHour';
+import sub from 'date-fns/sub';
import { ViewContext } from 'view';
import { dispatchMessageEvent } from '../components/message';
import { localize } from '../localize/localize';
@@ -6,7 +11,11 @@ import { CameraConfig, ExtendedHomeAssistant, FrigateBrowseMediaSource } from '.
import { View } from '../view';
import { formatDateAndTime, prettifyTitle } from './basic';
import { getRecordingMediaContentID } from './frigate';
-import { createChild, createEventParentForChildren, sortYoungestToOldest } from './ha/browse-media';
+import {
+ createChild,
+ createEventParentForChildren,
+ sortYoungestToOldest,
+} from './ha/browse-media';
import {
RecordingSegmentsItem,
sortOldestToYoungest,
@@ -23,7 +32,7 @@ import { getAllDependentCameras, getTrueCameras } from './camera.js';
* @param view The current view.
* @param options A set of cameraIDs to fetch recordings for, and a targetView to dispatch to.
*/
- export const changeViewToRecentRecordingForCameraAndDependents = async (
+export const changeViewToRecentRecordingForCameraAndDependents = async (
element: HTMLElement,
hass: ExtendedHomeAssistant,
dataManager: DataManager,
@@ -35,22 +44,15 @@ import { getAllDependentCameras, getTrueCameras } from './camera.js';
): Promise => {
const now = new Date();
- await changeViewToRecording(
- element,
- hass,
- dataManager,
- cameras,
- view,
- {
- ...options,
+ await changeViewToRecording(element, hass, dataManager, cameras, view, {
+ ...options,
- // Fetch 1 days worth of recordings (including recordings that are for the current hour).
- cameraIDs: getAllDependentCameras(cameras, view.camera),
- start: sub(now, { days: 1 }),
- end: add(now, { hours: 1 }),
- }
- );
-}
+ // Fetch 1 days worth of recordings (including recordings that are for the current hour).
+ cameraIDs: getAllDependentCameras(cameras, view.camera),
+ start: sub(now, { days: 1 }),
+ end: add(now, { hours: 1 }),
+ });
+};
/**
* Change the view to a recording.
@@ -132,9 +134,7 @@ const createRecordingChildren = (
): FrigateBrowseMediaSource[] => {
const children: FrigateBrowseMediaSource[] = [];
- for (const cameraID of getTrueCameras(
- cameras, cameraIDs
- )) {
+ for (const cameraID of getTrueCameras(cameras, cameraIDs)) {
const config = cameras.get(cameraID) ?? null;
const recordingSummary = dataManager.getRecordingSummaryForCamera(cameraID);
if (!config?.frigate.camera_name || !recordingSummary) {
diff --git a/src/utils/task.ts b/src/utils/task.ts
index afb91518..155958b6 100644
--- a/src/utils/task.ts
+++ b/src/utils/task.ts
@@ -4,6 +4,7 @@ import {
dispatchFrigateCardErrorEvent,
renderProgressIndicator,
} from '../components/message';
+import { CardWideConfig } from '../types';
import { errorToConsole } from './basic';
/**
@@ -18,11 +19,19 @@ export const renderTask = (
host: EventTarget,
task: Task,
completeFunc: (result: R) => TemplateResult | void,
- inProgressFunc?: () => TemplateResult | void,
+ options?: {
+ cardWideConfig?: CardWideConfig;
+ inProgressFunc?: () => TemplateResult | void;
+ },
): TemplateResult => {
+ const progressConfig = {
+ ...(options?.cardWideConfig && { cardWideConfig: options.cardWideConfig }),
+ };
return html` ${task.render({
- initial: () => inProgressFunc?.() ?? renderProgressIndicator(),
- pending: () => inProgressFunc?.() ?? renderProgressIndicator(),
+ initial: () =>
+ options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
+ pending: () =>
+ options?.inProgressFunc?.() ?? renderProgressIndicator(progressConfig),
error: (e: unknown) => {
errorToConsole(e as Error);
dispatchFrigateCardErrorEvent(host, e as Error);
diff --git a/src/view.ts b/src/view.ts
index 3d82ce5d..fcc5177f 100644
--- a/src/view.ts
+++ b/src/view.ts
@@ -13,7 +13,6 @@ export interface ViewEvolveParameters {
camera?: string;
target?: FrigateBrowseMediaSource | null;
childIndex?: number | null;
- previous?: View | null;
context?: ViewContext | null;
}
@@ -27,7 +26,6 @@ export class View {
public camera: string;
public target: FrigateBrowseMediaSource | null;
public childIndex: number | null;
- public previous: View | null;
public context: ViewContext | null;
constructor(params: ViewParameters) {
@@ -35,7 +33,6 @@ export class View {
this.camera = params.camera;
this.target = params.target ?? null;
this.childIndex = params.childIndex ?? null;
- this.previous = params.previous ?? null;
this.context = params.context ?? null;
}
@@ -90,7 +87,6 @@ export class View {
camera: this.camera,
target: this.target,
childIndex: this.childIndex,
- previous: this.previous,
context: this.context,
});
}
@@ -107,10 +103,6 @@ export class View {
target: params.target !== undefined ? params.target : this.target,
childIndex: params.childIndex !== undefined ? params.childIndex : this.childIndex,
context: params.context !== undefined ? params.context : this.context,
-
- // Special case: Set the previous to this of the evolved view (rather than
- // the previous of this).
- previous: params.previous !== undefined ? params.previous : this,
});
}
@@ -119,7 +111,7 @@ export class View {
* @param context The context to merge in.
* @returns This view.
*/
- public mergeInContext(context: ViewContext): View {
+ public mergeInContext(context?: ViewContext): View {
this.context = { ...this.context, ...context };
return this;
}
diff --git a/tsconfig.json b/tsconfig.json
index 0b7df973..b5045a7e 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "es2017",
+ "target": "es2021",
"module": "es2020",
"moduleResolution": "node",
- "lib": ["es2017", "dom", "dom.iterable"],
+ "lib": ["es2021", "dom", "dom.iterable"],
"noEmit": true,
"noUnusedParameters": true,
"noImplicitReturns": true,