feat: Add richer card theme support (#1809)

This commit is contained in:
Dermot Duffy authored and GitHub committed 2025-01-07 19:54:13 -08:00
1 parent f7ecb91578
commit 278560b5ac
65 files changed
+1187 -352

No files matched your search

+1 -2
View File
@@ -97,7 +97,6 @@ export class ConfigManager {
this._api.getViewManager().reset();
this._api.getMessageManager().reset();
this._api.getStyleManager().setPerformance();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
setKeyboardShortcutsFromConfig(this._api, this);
@@ -134,7 +133,7 @@ export class ConfigManager {
const previousConfig = this._overriddenConfig;
this._overriddenConfig = overriddenConfig;
this._api.getStyleManager().setMinMaxHeight();
this._api.getStyleManager().updateFromConfig();
if (
previousConfig &&
+2 -2
View File
@@ -53,8 +53,8 @@ export class HASSManager {
});
}
// Dark mode may depend on HASS.
this._api.getStyleManager().setLightOrDarkMode();
// Theme may depend on HASS.
this._api.getStyleManager().applyTheme();
this._stateWatcher.setHASS(oldHass, hass);
}
+16 -11
View File
@@ -1,10 +1,12 @@
import throttle from 'lodash-es/throttle';
import { setOrRemoveAttribute } from '../utils/basic';
import { Timer } from '../utils/timer';
import { CardInteractionAPI } from './types';
export class InteractionManager {
protected _timer = new Timer();
protected _api: CardInteractionAPI;
protected _interacted = false;
constructor(api: CardInteractionAPI) {
this._api = api;
@@ -17,29 +19,32 @@ export class InteractionManager {
}, 1 * 1000);
public initialize(): void {
this._api.getConditionsManager().setState({ interaction: false });
this._setInteraction(false);
}
public hasInteraction(): boolean {
return this._timer.isRunning();
return this._interacted;
}
protected _setInteraction(val: boolean): void {
this._interacted = val;
setOrRemoveAttribute(
this._api.getCardElementManager().getElement(),
val,
'interaction',
);
this._api.getConditionsManager().setState({ interaction: val });
}
/**
* Start the user interaction ('screensaver') timer to reset the view to
* default `view.interaction_seconds` after user interaction.
*/
protected _reportInteraction(): void {
this._timer.stop();
this._setInteraction(true);
const timeoutSeconds = this._api.getConfigManager().getConfig()
?.view.interaction_seconds;
if (timeoutSeconds) {
this._api.getConditionsManager().setState({ interaction: true });
this._timer.start(timeoutSeconds, () => {
this._api.getConditionsManager().setState({ interaction: false });
this._api.getStyleManager().setLightOrDarkMode();
this._setInteraction(false);
});
}
}
+48 -18
View File
@@ -1,5 +1,10 @@
import { StyleInfo } from 'lit/directives/style-map';
import { FrigateCardConfig } from '../config/types';
import {
FrigateCardConfig,
frigateCardConfigDefaults,
ThemeConfig,
ThemeName,
} from '../config/types';
import { aspectRatioToStyle, setOrRemoveAttribute } from '../utils/basic';
import { View } from '../view/view';
import { CardStyleAPI } from './types';
@@ -11,21 +16,6 @@ export class StyleManager {
this._api = api;
}
public setLightOrDarkMode = (): void => {
const config = this._api.getConfigManager().getConfig();
const isDarkMode =
config?.view.dark_mode === 'on' ||
(config?.view.dark_mode === 'auto' &&
(!this._api.getInteractionManager().hasInteraction() ||
!!this._api.getHASSManager().getHASS()?.themes.darkMode));
setOrRemoveAttribute(
this._api.getCardElementManager().getElement(),
isDarkMode,
'dark',
);
};
public setExpandedMode(): void {
const card = this._api.getCardElementManager().getElement();
const view = this._api.getViewManager().getView();
@@ -58,7 +48,47 @@ export class StyleManager {
);
}
public setMinMaxHeight(): void {
public updateFromConfig(): void {
this.applyTheme();
this._setMinMaxHeight();
this._setPerformance();
this._setDimmable();
}
public applyTheme() {
const themeConfig = this._api.getConfigManager().getConfig()?.view.theme;
if (!themeConfig) {
return;
}
const element = this._api.getCardElementManager().getElement();
const themes = this._getThemeNames(themeConfig);
setOrRemoveAttribute(element, !!themes, 'themes', themes?.join(' '));
if (themeConfig.overrides) {
for (const [key, value] of Object.entries(themeConfig.overrides)) {
element.style.setProperty(key, value);
}
}
}
protected _getThemeNames(themeConfig: ThemeConfig): ThemeName[] | null {
return themeConfig.themes.length
? themeConfig.themes
: frigateCardConfigDefaults.view.theme.themes;
}
protected _setDimmable(): void {
const config = this._api.getConfigManager().getConfig();
setOrRemoveAttribute(
this._api.getCardElementManager().getElement(),
!!config?.view.dim,
'dimmable',
);
}
protected _setMinMaxHeight(): void {
const config = this._api.getConfigManager().getConfig();
if (config) {
const card = this._api.getCardElementManager().getElement();
@@ -66,7 +96,7 @@ export class StyleManager {
}
}
public setPerformance(): void {
protected _setPerformance(): void {
const STYLE_DISABLE_MAP = {
box_shadow: 'none',
border_radius: '0px',
+1
View File
@@ -180,6 +180,7 @@ export interface CardInitializerAPI {
}
export interface CardInteractionAPI {
getCardElementManager(): CardElementManager;
getConditionsManager(): ConditionsManager;
getConfigManager(): ConfigManager;
getStyleManager(): StyleManager;
+15 -6
View File
@@ -218,12 +218,14 @@ class FrigateCard extends LitElement {
const getContents = (kind: 'overlay' | 'outerlay'): TemplateResult => {
const shouldRenderMenu =
menuStyle !== 'none' &&
((menuStyle === 'outside' && kind === 'outerlay') ||
((menuStyle === 'outside' && kind === 'outerlay' && menuPosition === position) ||
(menuStyle !== 'outside' && kind === 'overlay'));
const shouldRenderStatusBar =
statusBarStyle !== 'none' &&
((statusBarStyle === 'outside' && kind === 'outerlay') ||
((statusBarStyle === 'outside' &&
kind === 'outerlay' &&
statusBarPosition === position) ||
(statusBarStyle !== 'outside' && kind === 'overlay'));
// Complex logic to try to always put the menu in the right-looking place.
@@ -233,8 +235,7 @@ class FrigateCard extends LitElement {
(menuPosition === 'bottom' &&
menuStyle === 'hidden' &&
statusBarStyle !== 'popup') ||
(menuPosition === 'top' &&
(menuStyle !== 'hidden' || statusBarStyle === 'popup'));
(menuPosition === 'top' && statusBarStyle === 'popup');
return html`
${shouldRenderMenu && renderMenuFirst ? this._renderMenu(menuPosition) : ''}
@@ -329,12 +330,20 @@ class FrigateCard extends LitElement {
return;
}
const outerlayUsed =
this._config?.menu.style === 'outside' ||
this._config?.status_bar.style === 'outside';
const mainClasses = {
main: true,
'curve-top':
this._config?.menu.style !== 'outside' || this._config?.menu.position !== 'top',
!outerlayUsed ||
(this._config?.menu.position !== 'top' &&
this._config?.status_bar.position !== 'top'),
'curve-bottom':
this._config?.menu.style !== 'outside' || this._config?.menu.position === 'top',
!outerlayUsed ||
(this._config?.menu.position !== 'bottom' &&
this._config?.status_bar.position !== 'bottom'),
};
const actions = this._controller.getActionsManager().getMergedActions();
+4 -1
View File
@@ -1,9 +1,10 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
import irisSVG from '../images/iris.svg';
import { Icon } from '../types';
import { HassEntity } from 'home-assistant-js-websocket';
export class IconController {
public getCustomIcon(icon?: Icon): string | null {
@@ -14,6 +15,8 @@ export class IconController {
return motioneyeSVG;
case 'reolink':
return reolinkSVG;
case 'iris':
return irisSVG;
default:
return null;
}
+2 -2
View File
@@ -647,11 +647,11 @@ export class MenuButtonController {
if (critical) {
return {
animation: 'pulse 3s infinite',
color: 'var(--error-color, white)',
color: 'var(--frigate-card-menu-button-critical-color)',
};
}
return {
color: 'var(--primary-color, white)',
color: 'var(--frigate-card-menu-button-active-color)',
};
}
@@ -67,6 +67,10 @@ export class StatusBarController {
this._host.requestUpdate();
}
public getConfig(): StatusBarConfig | null {
return this._config;
}
public shouldRender(): boolean {
return this._items.some((item) => item.enabled !== false && item.sufficient);
}
+40 -13
View File
@@ -1,7 +1,13 @@
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import {
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
unsafeCSS,
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { IconController } from '../components-lib/icon-controller';
import iconStyle from '../scss/icon.scss';
import { Icon } from '../types';
@@ -14,17 +20,35 @@ export class FrigateCardIcon extends LitElement {
@property({ attribute: false })
public icon?: Icon;
// Note: This attribute will allow non-active entity state styles (e.g. 'off',
// 'unavailable') to be overriden from outside the icon itself. This is useful
// in the menu / submenus where we want icons to follow menu theming, unless
// they are 'active'. This attribute is not used in code, but matched in
// icon.scss .
@property({ attribute: 'allow-override-non-active-styles', type: Boolean })
public allowOverrideNonActiveStyles = false;
private _controller = new IconController();
private _svg: HTMLElement | null = null;
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('icon')) {
const customIcon = this._controller.getCustomIcon(this.icon);
if (customIcon) {
const svgElement = document.createElement('svg');
svgElement.innerHTML = customIcon;
this._svg = svgElement;
} else {
this._svg = null;
}
}
}
protected render(): TemplateResult {
const customIconURL = this._controller.getCustomIcon(this.icon);
const style = styleMap(this.icon?.style || {});
if (customIconURL) {
return html`<img style="${style}" src="${customIconURL}" />`;
}
if (this.icon?.icon) {
return html`<ha-icon style="${style}" icon="${this.icon.icon}"></ha-icon>`;
if (this._svg) {
// Use SVG objects (rather than <img>) to ensure styling applies
// correctly.
return html`${this._svg}`;
}
if (this.hass && this.icon?.entity) {
const stateObj = this._controller.createStateObjectForStateBadge(
@@ -35,16 +59,19 @@ export class FrigateCardIcon extends LitElement {
// As a special case, need to pass in a color in order for the state
// color to be overridden.
return html`<state-badge
style="${style}"
.color="${this.icon.style?.color ?? undefined}"
.color="${this.style.color ?? undefined}"
.stateColor=${this.icon.stateColor ?? true}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${this.icon.icon}
></state-badge>`;
}
}
if (this.icon?.icon) {
return html`<ha-icon icon="${this.icon.icon}"></ha-icon>`;
}
if (this.icon?.fallback) {
return html`<ha-icon style="${style}" icon="${this.icon.fallback}"></ha-icon>`;
return html`<ha-icon icon="${this.icon.fallback}"></ha-icon>`;
}
return html``;
}
+2 -2
View File
@@ -1,12 +1,12 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement } from 'lit/decorators.js';
import irisLogo from '../images/camera-iris-transparent.svg';
import loadingStyle from '../scss/loading.scss';
import './icon';
@customElement('frigate-card-loading')
export class FrigateCardLoading extends LitElement {
protected render(): TemplateResult {
return html` <img src="${irisLogo}" /> `;
return html`<frigate-card-icon .icon=${{ icon: 'iris' }}></frigate-card-icon>`;
}
static get styles(): CSSResultGroup {
+55 -4
View File
@@ -66,8 +66,6 @@ export class FrigateCardMenu extends LitElement {
: button.title;
return html` <ha-icon-button
class="button"
style="${styleMap(button.style || {})}"
.actionHandler=${actionHandler({
hasHold: frigateCardHasAction(button.hold_action),
hasDoubleClick: frigateCardHasAction(button.double_tap_action),
@@ -76,18 +74,70 @@ export class FrigateCardMenu extends LitElement {
@action=${(ev) => this._controller.actionHandler(ev, button)}
>
<frigate-card-icon
?allow-override-non-active-styles=${true}
style="${styleMap(button.style || {})}"
.hass=${this.hass}
.icon=${{
icon: button.icon,
entity: button.entity,
stateColor: button.state_color,
style: button.style,
fallback: 'mdi:gesture-tap-button',
}}
></frigate-card-icon>
</ha-icon-button>`;
}
/** Theme-related styling is dynamically injected into the menu depending on
* the configured position, style and alignment to allow precise theming.
* The alternative is a massive (post-sass processing) CSS file would need to
* be shipped to account for every possible combination.
*
* Each rule uses 'var' values that have nested fallbacks of decreasing
* specificity, so the most specific theme variable will match, followed by
* the next most specific, etc.
*/
protected _renderPerInstanceStyle(): TemplateResult | void {
const config = this._controller.getMenuConfig();
if (!config) {
return;
}
const position = config.position;
const style = config.style;
const alignment = config.alignment;
const generateValue = (suffix: string): string => {
return `
var(--frigate-card-menu-override-${suffix},
var(--frigate-card-menu-position-${position}-alignment-${alignment}-style-${style}-${suffix},
var(--frigate-card-menu-position-${position}-alignment-${alignment}-${suffix},
var(--frigate-card-menu-position-${position}-${suffix},
var(--frigate-card-menu-style-${style}-${suffix},
var(--frigate-card-menu-alignment-${alignment}-${suffix},
var(--frigate-card-menu-${suffix})))))))`;
};
// By definition `rule` will match the current configuration, the choice is
// actually which of the var(...) variables will be used after the match.
const expandedRule = style === 'hidden' ? '[expanded]' : '';
const rule =
`[data-position='${position}']` +
`[data-style='${style}']` +
`[data-alignment='${alignment}']` +
expandedRule;
return html`<style>
:host(${rule}) {
background: ${generateValue('background')};
ha-icon-button {
color: ${generateValue('button-inactive-color')};
background: ${generateValue('button-background')};
}
}
</style>`;
}
protected render(): TemplateResult | void {
const config = this._controller.getMenuConfig();
const style = config?.style;
@@ -97,7 +147,8 @@ export class FrigateCardMenu extends LitElement {
const matchingButtons = this._controller.getButtons('matching');
const opposingButtons = this._controller.getButtons('opposing');
return html` <div
return html` ${this._renderPerInstanceStyle()}
<div
class="matching"
style="${styleMap({ flex: String(matchingButtons.length) })}"
>
-1
View File
@@ -63,7 +63,6 @@ export class FrigateCardNextPreviousControl extends LitElement {
right: this.side === 'right',
thumbnails: !renderIcon,
icons: renderIcon,
button: renderIcon,
};
if (renderIcon) {
+36
View File
@@ -36,12 +36,48 @@ export class FrigateCardStatusBar extends LitElement {
}
}
/** Theme-related styling is dynamically injected into the status bar depending on
* the configured position and style to allow precise theming.
*
* Each rule uses 'var' values that have nested fallbacks of decreasing
* specificity, so the most specific theme variable will match, followed by
* the next most specific, etc.
*/
protected _renderPerInstanceStyle(): TemplateResult | void {
const config = this._controller.getConfig();
if (!config) {
return;
}
const position = config.position;
const style = config.style;
const generateValue = (suffix: string): string => {
return `
var(--frigate-card-status-bar-override-${suffix},
var(--frigate-card-status-bar-position-${position}-style-${style}-${suffix},
var(--frigate-card-status-bar-position-${position}-${suffix},
var(--frigate-card-status-bar-style-${style}-${suffix},
var(--frigate-card-status-bar-${suffix})))))`;
};
const rule = `[data-position='${position}']` + `[data-style='${style}']`;
return html`<style>
:host(${rule}) {
color: ${generateValue('color')};
background: ${generateValue('background')};
}
</style>`;
}
protected render(): TemplateResult | void {
if (!this._controller.shouldRender()) {
return;
}
return html`
${this._renderPerInstanceStyle()}
<div class="status">
${this._controller.getRenderItems().map((item): TemplateResult | void => {
if (item.enabled === false) {
+7 -7
View File
@@ -43,9 +43,9 @@ export class FrigateCardSubmenu extends LitElement {
}
const title = item.title ?? getEntityTitle(this.hass, item.entity);
const style = styleMap(item.style || {});
return html`
<mwc-list-item
style="${styleMap(item.style || {})}"
graphic=${ifDefined(item.icon || item.entity ? 'icon' : undefined)}
?twoline=${!!item.subtitle}
?selected=${item.selected}
@@ -61,17 +61,18 @@ export class FrigateCardSubmenu extends LitElement {
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
})}
>
<span>${title ?? ''}</span>
${item.subtitle ? html`<span slot="secondary">${item.subtitle}</span>` : ''}
<span style="${style}">${title ?? ''}</span>
${item.subtitle
? html`<span slot="secondary" style="${style}">${item.subtitle}</span>`
: ''}
<frigate-card-icon
slot="graphic"
.hass=${this.hass}
.icon=${{
icon: item.icon,
entity: item.entity,
style: item.style,
}}
style="${styleMap(item.style || {})}"
style="${style}"
></frigate-card-icon>
</mwc-list-item>
`;
@@ -96,7 +97,6 @@ export class FrigateCardSubmenu extends LitElement {
>
<ha-icon-button
style="${style}"
class="button"
slot="trigger"
.label=${this.submenu.title || ''}
.actionHandler=${actionHandler({
@@ -110,6 +110,7 @@ export class FrigateCardSubmenu extends LitElement {
})}
>
<frigate-card-icon
?allow-override-non-active-styles=${true}
style="${style}"
.hass=${this.hass}
.icon=${typeof this.submenu.icon === 'string'
@@ -217,7 +218,6 @@ export class FrigateCardSubmenuSelect extends LitElement {
icon: this.submenuSelect.icon,
entity: entityID,
fallback: 'mdi:format-list-bulleted',
style: this.submenuSelect.style,
},
type: 'custom:frigate-card-menu-submenu',
+6
View File
@@ -26,6 +26,7 @@ import {
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_DIM,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
@@ -881,4 +882,9 @@ const UPGRADES = [
},
upgradeMoveToWithOverrides('dimensions.max_height', CONF_DIMENSIONS_HEIGHT),
deleteWithOverrides('dimensions.min_height'),
// v6.1.2+
upgradeMoveToWithOverrides('view.dark_mode', CONF_VIEW_DIM, {
transform: (val) => val === 'on',
}),
];
+18 -3
View File
@@ -1515,7 +1515,10 @@ const viewConfigDefault = {
interaction_mode: 'inactive' as const,
},
default_cycle_camera: false,
dark_mode: 'off' as const,
dim: false,
theme: {
themes: ['traditional' as const],
},
triggers: {
show_trigger_status: false,
filter_selected_camera: true,
@@ -1553,6 +1556,15 @@ export const triggersSchema = z.object({
});
export type TriggersOptions = z.infer<typeof triggersSchema>;
const themeName = z.enum(['ha', 'dark', 'light', 'traditional']);
export type ThemeName = z.infer<typeof themeName>;
const themeConfigSchema = z.object({
themes: themeName.array().default(viewConfigDefault.theme.themes),
overrides: z.record(z.string()).optional(),
});
export type ThemeConfig = z.infer<typeof themeConfigSchema>;
const viewConfigSchema = z
.object({
default: z
@@ -1578,7 +1590,10 @@ const viewConfigSchema = z
.default(viewConfigDefault.default_reset),
render_entities: z.string().array().optional(),
dark_mode: z.enum(['on', 'off', 'auto']).optional(),
theme: themeConfigSchema.default(viewConfigDefault.theme),
dim: z.boolean().default(viewConfigDefault.dim),
triggers: triggersSchema.default(viewConfigDefault.triggers),
keyboard_shortcuts: keyboardShortcutsSchema.default(
viewConfigDefault.keyboard_shortcuts,
@@ -1721,7 +1736,7 @@ const statusBarItemDefault = {
};
const statusBarConfigDefault = {
height: 30,
height: 40,
items: {
engine: statusBarItemDefault,
resolution: statusBarItemDefault,
+2 -1
View File
@@ -117,8 +117,8 @@ export const CONF_ELEMENTS = 'elements' as const;
const CONF_VIEW = 'view' as const;
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
export const CONF_VIEW_DARK_MODE = `${CONF_VIEW}.dark_mode` as const;
export const CONF_VIEW_DEFAULT = `${CONF_VIEW}.default` as const;
export const CONF_VIEW_DIM = `${CONF_VIEW}.dim` as const;
export const CONF_VIEW_INTERACTION_SECONDS = `${CONF_VIEW}.interaction_seconds` as const;
export const CONF_VIEW_KEYBOARD_SHORTCUTS = `${CONF_VIEW}.keyboard_shortcuts` as const;
export const CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED =
@@ -148,6 +148,7 @@ export const CONF_VIEW_DEFAULT_RESET_ENTITIES =
`${CONF_VIEW_DEFAULT_RESET}.entities` as const;
export const CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION =
`${CONF_VIEW_DEFAULT_RESET}.after_interaction` as const;
export const CONF_VIEW_THEME_THEMES = `${CONF_VIEW}.theme.themes` as const;
export const CONF_VIEW_TRIGGERS = `${CONF_VIEW}.triggers` as const;
export const CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS =
`${CONF_VIEW_TRIGGERS}.show_trigger_status` as const;
+15 -12
View File
@@ -200,7 +200,6 @@ import {
CONF_TIMELINE_STYLE,
CONF_TIMELINE_WINDOW_SECONDS,
CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
CONF_VIEW_DEFAULT_RESET,
@@ -208,6 +207,7 @@ import {
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_DIM,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_KEYBOARD_SHORTCUTS,
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
@@ -218,6 +218,7 @@ import {
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
CONF_VIEW_THEME_THEMES,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS,
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
@@ -552,13 +553,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'stack', label: localize('config.common.timeline.styles.stack') },
];
protected _darkModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'on', label: localize('config.view.dark_modes.on') },
{ value: 'off', label: localize('config.view.dark_modes.off') },
{ value: 'auto', label: localize('config.view.dark_modes.auto') },
];
protected _mediaActionNegativeConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{
@@ -912,6 +906,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') },
];
protected _themes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'ha', label: localize('config.view.theme.themes.ha') },
{ value: 'dark', label: localize('config.view.theme.themes.dark') },
{ value: 'light', label: localize('config.view.theme.themes.light') },
{ value: 'traditional', label: localize('config.view.theme.themes.traditional') },
];
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the full configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -1928,7 +1930,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
? html` <div class="values">
<div class="controls">
<ha-icon-button
class="button"
.label=${localize('editor.move_up')}
.disabled=${addNewCamera ||
!this._config ||
@@ -1950,7 +1951,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
></frigate-card-icon>
</ha-icon-button>
<ha-icon-button
class="button"
.label=${localize('editor.move_down')}
.disabled=${addNewCamera ||
!this._config ||
@@ -1975,7 +1975,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
></frigate-card-icon>
</ha-icon-button>
<ha-icon-button
class="button"
.label=${localize('editor.delete')}
.disabled=${addNewCamera}
@click=${() => {
@@ -2525,7 +2524,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_VIEW_CAMERA_SELECT,
this._cameraSelectViewModes,
)}
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
${this._renderSwitch(CONF_VIEW_DIM, this._defaults.view.dim)}
${this._renderNumberInput(CONF_VIEW_INTERACTION_SECONDS)}
${this._renderSwitch(
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
@@ -2533,6 +2532,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
${this._renderViewDefaultResetMenu()} ${this._renderViewTriggersMenu()}
${this._renderViewKeyboardShortcutMenu()}
${this._renderOptionSelector(CONF_VIEW_THEME_THEMES, this._themes, {
label: localize('config.view.theme.themes.editor_label'),
multiple: true,
})}
</div>
`
: ''}
+6 -1
View File
@@ -22,8 +22,13 @@ $ convert -extent 2048x1152 -flop -gravity center 47543120431_b285c45ac8_k.jpg f
$ mogrify -strip -interlace Plane -quality 85% -scale 492x277 frigate-bird-in-sky.jpg
```
## camera-iris.svg
## iris.svg / iris-outline.svg
**Link**: https://pictogrammers.com/library/mdi/icon/camera-iris/
**License**: https://www.apache.org/licenses/LICENSE-2.0
**Image Formatting Process**:
- Outline addded manually
- Opacity adjustmented manually
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.73,15L9.83,21.76C10.53,21.91 11.25,22 12,22C14.4,22 16.6,21.15 18.32,19.75L14.66,13.4M2.46,15C3.38,17.92 5.61,20.26 8.45,21.34L12.12,15M8.54,12L4.64,5.25C3,7 2,9.39 2,12C2,12.68 2.07,13.35 2.2,14H9.69M21.8,10H14.31L14.6,10.5L19.36,18.75C21,16.97 22,14.6 22,12C22,11.31 21.93,10.64 21.8,10M21.54,9C20.62,6.07 18.39,3.74 15.55,2.66L11.88,9M9.4,10.5L14.17,2.24C13.47,2.09 12.75,2 12,2C9.6,2 7.4,2.84 5.68,4.25L9.34,10.6L9.4,10.5Z" /></svg>

Before

Width:  |  Height:  |  Size: 509 B

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="camera-iris.svg"
sodipodi:docname="iris-outline.svg"
id="svg4"
version="1.1"
viewBox="0 0 24 24"
@@ -21,9 +21,9 @@
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="26.62506"
inkscape:cx="-1.934268"
inkscape:cy="15.680716"
inkscape:zoom="19.378578"
inkscape:cx="8.2823415"
inkscape:cy="12.359008"
inkscape:window-width="3840"
inkscape:window-height="1527"
inkscape:window-x="1080"
@@ -34,16 +34,16 @@
id="g1119"
style="fill-opacity:0.05;fill:#ffffff">
<circle
style="fill:#ffffff;fill-opacity:0.05;stroke-width:1.39729"
style="display:inline;fill:#000000;fill-opacity:0.49782825;stroke-width:1.39729;opacity:0.5"
id="path170"
cx="12"
cy="12"
inkscape:label="White Background"
inkscape:label="Background"
r="11.25" />
<path
d="M 13.730001,15 9.8300003,21.76 C 10.53,21.91 11.25,22 12,22 c 2.400001,0 4.6,-0.85 6.32,-2.25 L 14.660001,13.4 M 2.4600003,15 c 0.92,2.92 3.15,5.26 5.99,6.34 L 12.12,15 m -3.5799997,-3 -3.9,-6.7499996 c -1.64,1.749999 -2.64,4.1399993 -2.64,6.7499996 0,0.68 0.07,1.35 0.2,2 h 7.49 M 21.8,9.9999997 H 14.310001 L 14.600001,10.5 19.36,18.75 C 21,16.97 22,14.6 22,12 22,11.31 21.93,10.64 21.8,9.9999997 m -0.26,-1 C 20.62,6.0700005 18.39,3.7400002 15.550001,2.6600002 L 11.88,8.9999997 M 9.4000003,10.5 14.170001,2.2400002 c -0.7,-0.15 -1.420001,-0.24 -2.170001,-0.24 -2.3999997,0 -4.5999997,0.84 -6.3199997,2.2500003 l 3.66,6.3499995 z"
id="path2"
inkscape:label="Iris"
style="fill-opacity:0.05;fill:#ffffff" />
style="fill-opacity:0.50221002;fill:#ffffff;opacity:0.75" />
</g>
</svg>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="camera-iris-transparent.svg"
id="svg4"
version="1.1"
viewBox="0 0 24 24"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#b93e3e"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.60784314"
inkscape:pagecheckerboard="false"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="19.378578"
inkscape:cx="12.100991"
inkscape:cy="12.307405"
inkscape:window-width="1440"
inkscape:window-height="848"
inkscape:window-x="0"
inkscape:window-y="52"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<g
id="g1119"
style="fill-opacity:0.05;fill:#ffffff;stroke-width:1.0015748;stroke-dasharray:none">
<circle
style="display:none;opacity:0.5;fill:#000000;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="path170"
cx="12"
cy="12"
inkscape:label="Background"
r="11.25" />
<path
d="M 13.730001,15 9.8300003,21.76 C 10.53,21.91 11.25,22 12,22 c 2.400001,0 4.6,-0.85 6.32,-2.25 L 14.660001,13.4 M 2.4600003,15 c 0.92,2.92 3.15,5.26 5.99,6.34 L 12.12,15 m -3.5799997,-3 -3.9,-6.7499996 c -1.64,1.749999 -2.64,4.1399993 -2.64,6.7499996 0,0.68 0.07,1.35 0.2,2 h 7.49 M 21.8,9.9999997 H 14.310001 L 14.600001,10.5 19.36,18.75 C 21,16.97 22,14.6 22,12 22,11.31 21.93,10.64 21.8,9.9999997 m -0.26,-1 C 20.62,6.0700005 18.39,3.7400002 15.550001,2.6600002 L 11.88,8.9999997 M 9.4000003,10.5 14.170001,2.2400002 c -0.7,-0.15 -1.420001,-0.24 -2.170001,-0.24 -2.3999997,0 -4.5999997,0.84 -6.3199997,2.2500003 l 3.66,6.3499995 z"
id="path2"
inkscape:label="Iris"
style="fill-opacity:1;fill:#ffffff;opacity:1;stroke-width:1.0015748;stroke-dasharray:none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "Visualitza les càmeres seleccionades recentment",
"dark_mode": "Mode fosc",
"dark_modes": {
"auto": "Automàtic",
"off": "Desactivat",
"on": "Activat"
},
"default": "Vista per defecte",
"default_cycle_camera": "Passeu per les càmeres quan s'actualitzi la vista predeterminada",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": ""
}
},
"dim": "",
"interaction_seconds": "Segons després de l'acció de l'usuari per continuar interactuant (0=mai)",
"keyboard_shortcuts": {
"editor_label": "",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"theme": {
"themes": {
"dark": "",
"ha": "",
"editor_label": "",
"light": "",
"traditional": ""
}
},
"triggers": {
"actions": {
"editor_label": "Activar accions",
+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "View for newly selected cameras",
"dark_mode": "Dark mode",
"dark_modes": {
"auto": "Auto",
"off": "Off",
"on": "On"
},
"default": "Default view",
"default_cycle_camera": "Cycle through cameras when default view updates",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": "Only reset when card has no human interaction"
}
},
"dim": "Dim when no interaction",
"interaction_seconds": "Seconds after user action to remain interacted with (0=never)",
"keyboard_shortcuts": {
"editor_label": "Keyboard shortcuts",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "PTZ Zoom In",
"ptz_zoom_out": "PTZ Zoom Out"
},
"theme": {
"themes": {
"dark": "Dark",
"ha": "Home Assistant",
"editor_label": "Themes",
"light": "Light",
"traditional": "Traditional"
}
},
"triggers": {
"actions": {
"editor_label": "Trigger actions",
+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "Afficher les caméras nouvellement sélectionnées",
"dark_mode": "Mode sombre",
"dark_modes": {
"auto": "Automatique",
"off": "Désactivé",
"on": "Activé"
},
"default": "Vue par défaut",
"default_cycle_camera": "Parcourez les caméras lorsque la vue par défaut est mise à jour",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": "Réinitialiser uniquement lorsque la carte n'a pas d'interaction humaine"
}
},
"dim": "",
"interaction_seconds": "Secondes après l'action de l'utilisateur pour maintenir l'interaction (0=jamais)",
"keyboard_shortcuts": {
"editor_label": "Raccourcis clavier",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "PTZ zoomer",
"ptz_zoom_out": "PTZ dézoomer"
},
"theme": {
"themes": {
"dark": "",
"ha": "",
"editor_label": "",
"light": "",
"traditional": ""
}
},
"triggers": {
"actions": {
"editor_label": "Actions de déclenchement",
+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "Visualizza per le telecamere appena selezionate",
"dark_mode": "Tema scuro",
"dark_modes": {
"auto": "auto",
"off": "Off",
"on": "On"
},
"default": "Visualizzazione predefinita",
"default_cycle_camera": "Scorri le telecamere quando si aggiorna la visualizzazione predefinita",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": ""
}
},
"dim": "",
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"theme": {
"themes": {
"dark": "",
"ha": "",
"editor_label": "",
"light": "",
"traditional": ""
}
},
"triggers": {
"actions": {
"editor_label": "",
+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro",
"dark_modes": {
"auto": "Automático",
"off": "Desligado",
"on": "Ligado"
},
"default": "Visualização padrão",
"default_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": ""
}
},
"dim": "",
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"theme": {
"themes": {
"dark": "",
"ha": "",
"editor_label": "",
"light": "",
"traditional": ""
}
},
"triggers": {
"actions": {
"editor_label": "",
+10 -6
View File
@@ -481,12 +481,6 @@
},
"view": {
"camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro",
"dark_modes": {
"auto": "Automático",
"off": "Desligado",
"on": "Ligado"
},
"default": "Visualização padrão",
"default_cycle_camera": "Percorrer as câmeras quando a visualização padrão for atualizada",
"default_reset": {
@@ -501,6 +495,7 @@
"inactive": ""
}
},
"dim": "",
"interaction_seconds": "",
"keyboard_shortcuts": {
"editor_label": "",
@@ -513,6 +508,15 @@
"ptz_zoom_in": "",
"ptz_zoom_out": ""
},
"theme": {
"themes": {
"dark": "",
"ha": "",
"editor_label": "",
"light": "",
"traditional": ""
}
},
"triggers": {
"actions": {
"editor_label": "",
+4 -4
View File
@@ -1,6 +1,7 @@
ha-icon-button.button {
color: var(--secondary-color, white);
background-color: rgba(0, 0, 0, 0.6);
ha-icon-button {
color: var(--frigate-card-button-color);
background-color: var(--frigate-card-button-background);
border-radius: 50%;
padding: 0px;
margin: 3px;
@@ -8,5 +9,4 @@ ha-icon-button.button {
/* Buttons can always be clicked */
pointer-events: auto;
opacity: 0.9;
}
+3 -1
View File
@@ -1,3 +1,5 @@
@use './themes/base.scss';
:host {
display: block;
@@ -36,7 +38,7 @@ frigate-card-loading {
z-index: 1;
}
:host([dark]) {
:host([dimmable]:not([interaction])) {
filter: brightness(75%);
}
:host([panel]:not([casted])) {
+2 -4
View File
@@ -37,9 +37,8 @@ div.control-surround {
}
frigate-card-icon.control {
color: var(--secondary-color, white);
background-color: rgba(0, 0, 0, 0.7);
opacity: 0.5;
color: var(--frigate-card-button-color);
background-color: var(--frigate-card-button-background);
pointer-events: all;
--mdc-icon-size: #{$drawer-icon-size};
@@ -54,7 +53,6 @@ frigate-card-icon.control:hover {
// When the drawer is open or hovered make the button to close it more
// prominent.
opacity: 1;
background-color: black;
}
:host([location='left']) frigate-card-icon.control {
+1 -1
View File
@@ -98,7 +98,7 @@ div.upgrade span {
margin-right: 0px;
margin-bottom: 5px;
}
.submenu .controls ha-icon-button.button {
.submenu .controls ha-icon-button {
--mdc-icon-button-size: 32px;
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
}
+10
View File
@@ -12,3 +12,13 @@
width: 100%;
box-sizing: border-box;
}
svg path {
// !important is necessary to overcome the fill set on the path itself.
fill: currentColor !important;
}
:host([allow-override-non-active-styles]) state-badge {
--state-inactive-color: inherit;
--state-unavailable-color: inherit;
}
+3 -2
View File
@@ -9,8 +9,9 @@
pointer-events: none;
}
img {
width: 10%;
frigate-card-icon {
height: 50%;
width: auto;
animation: rotate 8s linear infinite;
}
+2 -1
View File
@@ -1,4 +1,5 @@
$bg-img: url('../images/camera-iris-transparent.svg');
// Use the outlined Iris logo since the background color is unknown.
$bg-img: url('../images/iris-background.svg');
:host {
background-color: var(--primary-background-color);
+22 -41
View File
@@ -15,6 +15,10 @@
overflow: visible;
}
:host([data-style='outside']) {
width: 100%;
}
/*******************************************************
* Non-hover styles should not interact with the pointer
*******************************************************/
@@ -45,15 +49,6 @@ div.opposing {
justify-content: flex-end;
}
/********************
* Outside menu style
********************/
:host([data-style='outside']) {
width: 100%;
background: var(--secondary-background-color);
}
/********************************************************
* Hack: Ensure host & div expand for column flex layouts
********************************************************/
@@ -126,24 +121,6 @@ div.opposing {
height: auto;
}
:host([data-style='overlay'][data-position='top'][data-alignment='left']),
:host([data-style='overlay'][data-position='bottom'][data-alignment='left']),
:host([data-style*='hover'][data-position='top'][data-alignment='left']),
:host([data-style*='hover'][data-position='bottom'][data-alignment='left']),
:host([data-style='hidden'][data-position='top'][data-alignment='left'][expanded]),
:host([data-style='hidden'][data-position='bottom'][data-alignment='left'][expanded]) {
background: linear-gradient(90deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
}
:host([data-style='overlay'][data-position='top'][data-alignment='right']),
:host([data-style='overlay'][data-position='bottom'][data-alignment='right']),
:host([data-style*='hover'][data-position='top'][data-alignment='right']),
:host([data-style*='hover'][data-position='bottom'][data-alignment='right']),
:host([data-style='hidden'][data-position='top'][data-alignment='right'][expanded]),
:host([data-style='hidden'][data-position='bottom'][data-alignment='right'][expanded]) {
background: linear-gradient(270deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
}
:host([data-style='overlay'][data-position='left']),
:host([data-style='overlay'][data-position='right']),
:host([data-style*='hover'][data-position='left']),
@@ -154,20 +131,24 @@ div.opposing {
height: 100%;
}
:host([data-style='overlay'][data-position='left'][data-alignment='top']),
:host([data-style='overlay'][data-position='right'][data-alignment='top']),
:host([data-style*='hover'][data-position='left'][data-alignment='top']),
:host([data-style*='hover'][data-position='right'][data-alignment='top']),
:host([data-style='hidden'][data-position='left'][data-alignment='top'][expanded]),
:host([data-style='hidden'][data-position='right'][data-alignment='top'][expanded]) {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
/************
* Menu theme
************/
// When the menu uses the hidden style, and is not expanded, the remaining
// permanent menu buttons use the menu background color instead of the button
// background color.
:host([data-style='hidden']:not([expanded])) ha-icon-button {
background: var(--frigate-card-menu-background);
}
:host([data-style='overlay'][data-position='left'][data-alignment='bottom']),
:host([data-style='overlay'][data-position='right'][data-alignment='bottom']),
:host([data-style*='hover'][data-position='left'][data-alignment='bottom']),
:host([data-style*='hover'][data-position='right'][data-alignment='bottom']),
:host([data-style='hidden'][data-position='left'][data-alignment='bottom'][expanded]),
:host([data-style='hidden'][data-position='right'][data-alignment='bottom'][expanded]) {
background: linear-gradient(0deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
// Icons in the menu are expected to follow Frigate card theming unless they are
// active (in which case we want to take advantage of the whatever styling is
// appropriate, e.g. light icon partially lit).
:host ha-icon-button {
--state-unavailable-color: var(--frigate-card-button-color);
--state-inactive-color: var(--frigate-card-button-color);
}
// Further theme related styling is dynamically applied by `menu.ts`, see
// `_renderPerInstanceStyle`.
+13 -20
View File
@@ -1,7 +1,7 @@
@use './button.scss';
:host {
--mdc-icon-size: calc(var(--frigate-card-status-bar-height) - 6px);
--mdc-icon-size: calc(var(--frigate-card-status-bar-height) / 2);
display: block;
width: 100%;
@@ -20,23 +20,10 @@
pointer-events: none;
}
/***************
* Outside style
***************/
/**********************
* Popup style behavior
**********************/
:host([data-style='outside']) {
color: var(--primary-text-color);
background: var(--secondary-background-color);
}
/*******************
* Non-outside style
*******************/
:host(:not([data-style='outside'])) {
color: white;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
}
:host([data-style='popup'][hide]) {
opacity: 0;
}
@@ -63,8 +50,6 @@
margin: 3px 5px;
align-content: center;
opacity: 0.7;
}
.item:first-child {
margin-left: 10px;
@@ -81,7 +66,8 @@
cursor: pointer;
}
img.item {
img.item,
frigate-card-icon.item {
display: block;
// To ensure images render somewhat reasonably looking their height is kept to
@@ -89,3 +75,10 @@ img.item {
height: var(--mdc-icon-size, 24px);
width: auto;
}
/************************
* Status bar theme hooks
************************/
// Further theme related styling is dynamically applied by `status-bar.ts`, see
// `_renderPerInstanceStyle`.
+11
View File
@@ -7,3 +7,14 @@
mwc-list-item {
z-index: 20;
}
ha-icon-button {
// The main submenu icon is expected to follow Frigate card theming unless it
// is are active (in which case we want to take advantage of the whatever
// styling is appropriate, e.g. light icon partially lit).
--state-unavailable-color: var(--frigate-card-button-color);
--state-inactive-color: var(--frigate-card-button-color);
color: var(--frigate-card-menu-button-inactive-color);
background-color: var(--frigate-card-menu-button-background);
}
+127
View File
@@ -0,0 +1,127 @@
@use 'dark.scss';
@use 'light.scss';
@use 'traditional.scss';
:host {
/*********
* General
*********/
--frigate-card-exterior: var(--secondary-background-color, black);
--frigate-card-background: var(--primary-background-color, white);
--frigate-card-foreground-primary: var(--primary-color, black);
--frigate-card-active-color: var(--accent-color, orange);
--frigate-card-warning-color: var(--error-color, red);
--frigate-card-text-color: var(--primary-text-color);
--frigate-card-divider-color: var(--divider-color);
--frigate-card-control-background: var(--app-header-background-color);
--frigate-card-control-background-transparent: color-mix(
in oklab,
var(--frigate-card-control-background),
transparent 60%
);
--frigate-card-control-foreground: var(--app-header-text-color);
/*********
* Buttons
*********/
// The color of buttons (in general, menu or otherwise).
--frigate-card-button-color: var(--frigate-card-control-foreground);
// The color of button background "circles".
--frigate-card-button-background: var(--frigate-card-control-background-transparent);
/******
* Menu
******/
// The color of an active icon in the menu.
--frigate-card-menu-button-active-color: var(--frigate-card-active-color);
// The color of an inactive icon in the menu.
--frigate-card-menu-button-inactive-color: var(--frigate-card-button-color);
// The color of a critical icon in the menu.
--frigate-card-menu-button-critical-color: var(
--error-color,
var(--frigate-card-warning-color)
);
// The background color of the button "circles" in the menu.
--frigate-card-menu-button-background: color-mix(
in oklab,
var(--frigate-card-menu-button-inactive-color),
transparent 60%
);
// The menu background color.
--frigate-card-menu-background: var(--frigate-card-control-background-transparent);
--frigate-card-menu-style-outside-background: var(--frigate-card-control-background);
// May be overridden by the user to force a particular color.
--frigate-card-menu-override-background: unset;
--frigate-card-menu-override-button-inactive-color: unset;
--frigate-card-menu-override-button-background: unset;
// Examples of precise theming for the menu:
// --frigate-card-menu-position-left-alignment-left-style-overlay-background: pink;
// --frigate-card-menu-position-left-background: purple;
// --frigate-card-menu-style-overlay-background: pink;
/************
* Status bar
************/
// May be overridden by the user to force a particular color.
--frigate-card-status-bar-override-background: unset;
--frigate-card-status-bar-override-color: unset;
// The color of items on the status bar.
--frigate-card-status-bar-color: var(--frigate-card-control-foreground);
// The background color of the outerlay above/below the card.
--frigate-card-status-bar-background: var(
--frigate-card-control-background-transparent
);
--frigate-card-status-bar-style-outside-background: var(
--frigate-card-control-background
);
/************
* Thumbnails
************/
--frigate-card-thumbnail-icon-color: var(--frigate-card-foreground-primary);
--frigate-card-thumbnail-border-color: var(--frigate-card-foreground-primary);
--frigate-card-thumbnail-background: var(--frigate-card-background);
--frigate-card-thumbnail-icon-favorite-color: color-mix(
in oklab,
gold,
transparent 25%
);
--frigate-card-thumbnail-text-color: var(--frigate-card-text-color);
/**********
* Timeline
**********/
--frigate-card-timeline-text-color: var(--frigate-card-text-color);
--frigate-card-timeline-item-color: var(--frigate-card-foreground-primary);
--frigate-card-timeline-tool-color: var(--frigate-card-foreground-primary);
--frigate-card-timeline-background: var(--frigate-card-background);
// style for the 'background' style of item (i.e. recordings).
--frigate-card-timeline-background-item-color: color-mix(
in oklab,
var(--frigate-card-timeline-item-color),
transparent 90%
);
--frigate-card-timeline-divider-color: var(--frigate-card-divider-color);
--frigate-card-timeline-target-bar-color: var(--frigate-card-active-color);
}
+11
View File
@@ -0,0 +1,11 @@
:host([themes~='dark']) {
// This file should include only, and exactly match, values from
// https://github.com/home-assistant/frontend/blob/dev/src/resources/styles-data.ts .
--primary-background-color: #111111;
--secondary-background-color: #282828;
--primary-text-color: #e1e1e1;
--divider-color: rgba(225, 225, 225, 0.12);
--app-header-background-color: #101e24;
--app-header-text-color: #e1e1e1;
}
+17
View File
@@ -0,0 +1,17 @@
:host([themes~='light']) {
// This file should include only, and exactly match, values from
// https://github.com/home-assistant/frontend/blob/dev/src/resources/ha-style.ts
--primary-background-color: #fafafa;
--secondary-background-color: #e5e5e5;
--primary-text-color: #212121;
--divider-color: rgba(0, 0, 0, 0.12);
--app-header-text-color: #ffffff;
// HA dynamically sets these values based on choice of primary/secondary
// colors and hex blending techniques. The card does not currently
// re-implement this logic, rather just uses the default static value.
// See: https://github.com/home-assistant/frontend/blob/dev/src/common/dom/apply_themes_on_element.ts
--app-header-background-color: #03a9f4;
}
+99
View File
@@ -0,0 +1,99 @@
@mixin background-gradient($var, $angle) {
#{$var}: linear-gradient(
$angle,
var(--frigate-card-menu-background),
var(--frigate-card-menu-background),
var(--frigate-card-menu-background),
color-mix(in oklab, var(--frigate-card-menu-background), transparent 100%)
);
}
:host([themes~='traditional']) {
/*********
* General
*********/
--frigate-card-control-background: black;
/******
* Menu
******/
--frigate-card-menu-button-active-color: var(--primary-color, white);
--frigate-card-menu-button-background: rgba(0, 0, 0, 0.6);
@include background-gradient(
'--frigate-card-menu-position-left-alignment-top-background',
180deg
);
@include background-gradient(
'--frigate-card-menu-position-left-alignment-bottom-background',
0deg
);
@include background-gradient(
'--frigate-card-menu-position-right-alignment-top-background',
180deg
);
@include background-gradient(
'--frigate-card-menu-position-right-alignment-bottom-background',
0deg
);
@include background-gradient(
'--frigate-card-menu-position-top-alignment-left-background',
90deg
);
@include background-gradient(
'--frigate-card-menu-position-top-alignment-right-background',
270deg
);
@include background-gradient(
'--frigate-card-menu-position-bottom-alignment-left-background',
90deg
);
@include background-gradient(
'--frigate-card-menu-position-bottom-alignment-right-background',
270deg
);
--frigate-card-menu-position-top-alignment-left-style-outside-background: var(
--secondary-background-color
);
--frigate-card-menu-position-top-alignment-right-style-outside-background: var(
--secondary-background-color
);
--frigate-card-menu-position-bottom-alignment-left-style-outside-background: var(
--secondary-background-color
);
--frigate-card-menu-position-bottom-alignment-right-style-outside-background: var(
--secondary-background-color
);
/************
* Status bar
************/
--frigate-card-status-bar-color: white;
@include background-gradient(
'--frigate-card-status-bar-position-top-background',
90deg
);
@include background-gradient(
'--frigate-card-status-bar-position-bottom-background',
90deg
);
--frigate-card-status-bar-position-top-style-outside-background: var(
--secondary-background-color
);
--frigate-card-status-bar-position-bottom-style-outside-background: var(
--secondary-background-color
);
--frigate-card-status-bar-position-top-style-outside-color: var(
--secondary-text-color
);
--frigate-card-status-bar-position-bottom-style-outside-color: var(
--secondary-text-color
);
}
+2
View File
@@ -5,6 +5,8 @@
width: 100%;
height: 100%;
background-color: var(--frigate-card-thumbnail-background);
--frigate-card-carousel-thumbnail-opacity: 1;
}
+1 -1
View File
@@ -5,7 +5,7 @@
width: 100%;
margin-left: 5px;
padding: 5px;
color: var(--primary-text-color);
color: var(--frigate-card-thumbnail-text-color);
overflow: hidden;
column-gap: 5%;
}
+4 -5
View File
@@ -15,7 +15,7 @@
}
:host([details]) {
border: 1px solid var(--primary-color);
border: 1px solid var(--frigate-card-thumbnail-border-color);
border-radius: var(
--frigate-card-css-border-radius,
var(--ha-card-border-radius, 4px)
@@ -24,7 +24,7 @@
// When details are enabled, use a background color so that the details have
// contrast with the background.
background-color: var(--primary-background-color, black);
background-color: var(--frigate-card-thumbnail-background);
}
:host(:hover) {
@@ -34,8 +34,7 @@
frigate-card-icon {
position: absolute;
border-radius: 50%;
opacity: 0.5;
color: var(--primary-color);
color: var(--frigate-card-thumbnail-icon-color);
cursor: pointer;
@@ -52,7 +51,7 @@ frigate-card-icon.star {
left: 3px;
}
frigate-card-icon.star.starred {
color: gold;
color: var(--frigate-card-thumbnail-icon-favorite-color);
}
frigate-card-icon.timeline {
+31 -21
View File
@@ -10,6 +10,8 @@
// So that absolute sidedrawer is relative to this host.
position: relative;
background-color: var(--frigate-card-timeline-background);
}
:host(:not([mini])) {
@@ -30,7 +32,7 @@ div.timeline {
}
.vis-text {
color: var(--primary-text-color) !important;
color: var(--frigate-card-timeline-text-color) !important;
}
.vis-timeline {
@@ -39,7 +41,7 @@ div.timeline {
.vis-labelset .vis-label {
// Group labels.
color: var(--primary-text-color);
color: var(--frigate-card-timeline-text-color);
}
:host([ribbon]:not([groups])) .vis-item:not(.vis-background) {
@@ -52,12 +54,11 @@ div.timeline {
// Ribbon timelines have a much shorter 'bar'.
height: 6px;
}
.vis-item {
border-color: var(--primary-color);
border-color: var(--frigate-card-timeline-item-color);
background: none;
color: var(--primary-text-color);
background-color: var(--primary-color);
color: var(--frigate-card-timeline-text-color);
background-color: var(--frigate-card-timeline-item-color);
transition:
background-color ease-in-out 1s,
@@ -65,13 +66,15 @@ div.timeline {
box-shadow ease-in-out 1s;
}
.vis-item.vis-selected {
border-color: var(--accent-color);
background-color: var(--accent-color);
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
border-color: var(--frigate-card-active-color);
background-color: var(--frigate-card-active-color);
box-shadow: var(
--frigate-card-css-box-shadow,
0px 0px 5px 1px var(--frigate-card-active-color)
);
}
.vis-item.vis-background {
background-color: var(--primary-color);
opacity: 0.1;
background-color: var(--frigate-card-timeline-background-item-color);
}
// If there are no timeline groups shown (e.g. mini mode with a single camera),
@@ -103,16 +106,19 @@ div.timeline {
.vis-item.vis-cluster {
border-style: dotted;
color: var(--primary-text-color);
background-color: var(--primary-background-color);
box-shadow: var(--frigate-card-css-box-shadow, 0px 0px 5px 1px var(--primary-color));
color: var(--frigate-card-timeline-text-color);
background-color: var(--frigate-card-timeline-background);
box-shadow: var(
--frigate-card-css-box-shadow,
0px 0px 5px 1px var(--frigate-card-timeline-item-color)
);
}
.vis-item.vis-range {
border-radius: var(--frigate-card-css-border-radius, unset);
}
.vis-time-axis .vis-grid.vis-minor {
border-color: var(--secondary-color);
border-color: var(--frigate-card-timeline-divider-color);
}
// Give an indication that the user can interact with the axes.
@@ -121,11 +127,15 @@ div.timeline {
transition: background-color 0.5s ease-out;
}
:host([recordings]) .vis-text.vis-minor:hover {
background-color: var(--primary-color);
background-color: color-mix(
in oklab,
var(--frigate-card-timeline-text-color),
transparent 80%
);
}
.vis-time-axis .vis-grid.vis-major {
border-color: var(--secondary-color);
border-color: var(--frigate-card-timeline-divider-color);
}
.vis-label {
@@ -146,7 +156,7 @@ div.vis-tooltip {
}
.target_bar {
background-color: var(--primary-color);
background-color: var(--frigate-card-timeline-target-bar-color);
width: 2px;
z-index: 20;
@@ -155,8 +165,8 @@ div.vis-tooltip {
pointer-events: none;
}
.target_bar .vis-custom-time-marker {
background-color: var(--primary-background-color);
color: var(--primary-text-color);
background-color: var(--frigate-card-timeline-background);
color: var(--frigate-card-timeline-text-color);
bottom: 0px;
top: unset;
}
@@ -176,7 +186,7 @@ div.vis-tooltip {
position: absolute;
right: 0px;
bottom: 0px;
color: var(--primary-color);
color: var(--frigate-card-timeline-tool-color);
z-index: 10;
}
.timeline-tools * {
-4
View File
@@ -5,7 +5,6 @@ import {
LovelaceCardEditor,
Themes,
} from '@dermotduffy/custom-card-helpers';
import { StyleInfo } from 'lit/directives/style-map';
import { z } from 'zod';
export type ClipsOrSnapshots = 'clips' | 'snapshots';
@@ -146,9 +145,6 @@ export interface Icon {
// If an icon is not otherwise resolved / available, this will be used instead.
fallback?: string;
// Styling to apply to the icon.
style?: StyleInfo;
}
// *************************************************************************