feat: Add richer card theme support (#1809)
This commit is contained in:
@@ -11,13 +11,14 @@ view:
|
||||
| ------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `actions` | | [Actions](actions/README.md) to use for all views, individual actions may be overriden by view-specific actions. |
|
||||
| `camera_select` | `current` | The [view](view.md?id=supported-views) to show when a new camera is selected (e.g. in the camera menu). If `current` the view is unchanged when a new camera is selected. |
|
||||
| `dark_mode` | `off` | Whether or not to turn dark mode `on`, `off` or `auto` to automatically turn on if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time) or if dark mode is enabled in the HA profile theme setting. Dark mode dims the brightness by `25%`. |
|
||||
| `dim` | `false` | Whether or not to 'dim' the brightness of the card (by 25%) if the card `interaction_seconds` has expired (i.e. card has been left unattended for that period of time). |
|
||||
| `default` | `live` | The view to show in the card by default. The default camera is the first one listed. See [Supported Views](view.md?id=supported-views) below. |
|
||||
| `default_reset` | | The circumstances and behavior that cause the card to reset to the default view. See below. |
|
||||
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `reset_after_interaction` to reset the view after the interaction is complete. `0` means no interactions are reported / acted upon. |
|
||||
| `interaction_seconds` | `300` | After a mouse/touch interaction with the card, it will be considered "interacted with" until this number of seconds elapses without further interaction. May be used as part of an [interaction condition](conditions.md?id=interaction) or with `reset_after_interaction` to reset the view after the interaction is complete. |
|
||||
| `keyboard_shortcuts` | See [usage](../usage/keyboard-shortcuts.md) for defaults. | Configure keyboard shortcuts. See below. |
|
||||
| `render_entities` | | **YAML only**: A list of entity ids that should cause the card to re-render 'in-place'. The view/camera is not changed. This should **very** rarely be needed, but could be useful if the card is both setting and changing HA state of the same object as could be the case for some complex `card_mod` scenarios ([example](https://github.com/dermotduffy/frigate-hass-card/issues/343)). |
|
||||
| `reset_after_interaction` | `true` | If `true` the card will reset to the default configured view (i.e. 'screensaver' functionality) after `interaction_seconds` has elapsed after user interaction. |
|
||||
| `theme` | | How the card is themed. See below. |
|
||||
| `triggers` | | How to react when a camera is [triggered](cameras/README.md?id=triggers). |
|
||||
| `default_cycle_camera` | `false` | When set to `true` the selected camera is cycled on each default view change. |
|
||||
|
||||
@@ -63,6 +64,39 @@ Configure the key-bindings for the builtin keyboard shortcuts. See [usage](../us
|
||||
| alt | `false` | If `true` requires the `alt` key to be held. |
|
||||
| meta | `false` | If `true` requires the `meta` key to be held. |
|
||||
|
||||
## `theme` 🎨
|
||||
|
||||
All configuration is under:
|
||||
|
||||
```yaml
|
||||
view:
|
||||
theme: [...]
|
||||
```
|
||||
|
||||
Configure the theming/colors applied to the card.
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `themes` | `[traditional]` | A list of themes that are applied sequentially. Valid themes are shown below. Usually only a single value is needed. An empty list is treated the same as the default. |
|
||||
| `overrides` | | A list of CSS keys that can be used to tweak the theming. |
|
||||
|
||||
### `themes`
|
||||
|
||||
| Theme | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `dark` | Use a dark theme that is identical to the HA dark theme. |
|
||||
| `ha` | Uses HA-prescribed theming. Respects HA choice of dark or light colors. |
|
||||
| `light` | Use a light theme that is similar to the HA light theme (there are some differences if you do not use the standard choices of primary color). |
|
||||
| `traditional` | A theme based on the default Frigate card theme before full theming support was added. Respects HA color theme choices. |
|
||||
|
||||
### `overrides`
|
||||
|
||||
Allows overriding of any CSS value, can be used to tweak theming parameters.
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| Any CSS key. Overriding the [Frigate Card](https://github.com/dermotduffy/frigate-hass-card/tree/main/src/scss/themes/base.scss) CSS variables allows changing individual theming paramters, e.g. `--frigate-card-menu-override-background` | Any CSS value, e.g. `red` or `rgba(10, 11, 12, 0.64)`. |
|
||||
|
||||
## `triggers`
|
||||
|
||||
The `triggers` block controls how the card reacts when a camera is triggered (note that _what_ triggers the camera is controlled by the [`triggers`](cameras/README.md?id=triggers) block within the config for a given camera). This can be used for a variety of purposes, such as allowing the card to automatically change to `live` for a camera that triggers.
|
||||
@@ -136,7 +170,7 @@ view:
|
||||
interaction_mode: inactive
|
||||
render_entities:
|
||||
- switch.render_card
|
||||
dark_mode: 'off'
|
||||
dim: false
|
||||
triggers:
|
||||
show_trigger_status: false
|
||||
filter_selected_camera: true
|
||||
@@ -161,6 +195,12 @@ view:
|
||||
key: '-'
|
||||
ptz_home:
|
||||
key: 'h'
|
||||
theme:
|
||||
themes:
|
||||
- ha
|
||||
overrides:
|
||||
'--frigate-card-menu-button-active-color': red
|
||||
'--frigate-card-menu-position-left-style-overlay-alignment-left-background': pink
|
||||
actions:
|
||||
entity: light.office_main_lights
|
||||
tap_action:
|
||||
|
||||
@@ -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 |
+1
-1
@@ -31,7 +31,7 @@
|
||||
.svg-icon {
|
||||
background-color: rgb(255, 255, 255);
|
||||
vertical-align: middle;
|
||||
background: repeating-conic-gradient(#8080804f 0% 25%, transparent 0% 50%) 50% /
|
||||
background: repeating-conic-gradient(#80808091 0% 25%, transparent 0% 50%) 50% /
|
||||
4px 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,5 +8,6 @@ builtin custom items not found in the traditional [Material Design Icons
|
||||
| Icon Name | Resulting Icon |
|
||||
| ----------- | --------------------------------------------------------------------------- |
|
||||
| `frigate` |  |
|
||||
| `iris` |  |
|
||||
| `motioneye` |  |
|
||||
| `reolink` |  |
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"rollup-plugin-git-info": "^1.0.0",
|
||||
"rollup-plugin-serve": "^1.1.1",
|
||||
"rollup-plugin-styler": "^1.8.0",
|
||||
"rollup-plugin-svgo": "^2.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "patch:sass@npm%3A1.77.4#~/.yarn/patches/sass-npm-1.77.4-13b6910aea.patch",
|
||||
"semantic-release": "^24.1.1",
|
||||
|
||||
+3
-1
@@ -8,6 +8,7 @@ import typescript from '@rollup/plugin-typescript';
|
||||
import gitInfo from 'rollup-plugin-git-info';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
import styles from 'rollup-plugin-styler';
|
||||
import svgo from 'rollup-plugin-svgo';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
const watch = process.env.ROLLUP_WATCH === 'true' || process.env.ROLLUP_WATCH === '1';
|
||||
@@ -40,7 +41,8 @@ const plugins = [
|
||||
includePaths: ['./node_modules/'],
|
||||
},
|
||||
}),
|
||||
image(),
|
||||
svgo(),
|
||||
image({ exclude: '**/*.svg' }),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# This script copies icons out of the source tree for documentation.
|
||||
|
||||
FRIGATE="./src/camera-manager/frigate/assets/frigate.svg"
|
||||
IRIS="./src/images/iris.svg"
|
||||
MOTIONEYE="./src/camera-manager/motioneye/assets/motioneye.svg"
|
||||
REOLINK="./src/camera-manager/reolink/assets/reolink.svg"
|
||||
DIR_ICONS="./docs/images/icons"
|
||||
@@ -14,5 +15,6 @@ copy() {
|
||||
}
|
||||
|
||||
copy "$FRIGATE"
|
||||
copy "$IRIS"
|
||||
copy "$MOTIONEYE"
|
||||
copy "$REOLINK"
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -180,6 +180,7 @@ export interface CardInitializerAPI {
|
||||
}
|
||||
|
||||
export interface CardInteractionAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getStyleManager(): StyleManager;
|
||||
|
||||
+15
-6
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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``;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) })}"
|
||||
>
|
||||
|
||||
@@ -63,7 +63,6 @@ export class FrigateCardNextPreviousControl extends LitElement {
|
||||
right: this.side === 'right',
|
||||
thumbnails: !renderIcon,
|
||||
icons: renderIcon,
|
||||
button: renderIcon,
|
||||
};
|
||||
|
||||
if (renderIcon) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
`
|
||||
: ''}
|
||||
|
||||
@@ -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 +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>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -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 |
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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
@@ -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])) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 10%;
|
||||
frigate-card-icon {
|
||||
height: 50%;
|
||||
width: auto;
|
||||
animation: rotate 8s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--frigate-card-thumbnail-background);
|
||||
|
||||
--frigate-card-carousel-thumbnail-opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 * {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('ConfigManager', () => {
|
||||
expect(api.getViewManager().reset).toBeCalled();
|
||||
expect(api.getMessageManager().reset).toBeCalled();
|
||||
expect(api.getAutomationsManager().addAutomations).toBeCalled();
|
||||
expect(api.getStyleManager().setPerformance).toBeCalled();
|
||||
expect(api.getStyleManager().updateFromConfig).toBeCalled();
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('ConfigManager', () => {
|
||||
manager.computeOverrideConfig();
|
||||
|
||||
expect(manager.getConfig()).toBeNull();
|
||||
expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled();
|
||||
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore overrides with same config', () => {
|
||||
@@ -188,12 +188,12 @@ describe('ConfigManager', () => {
|
||||
vi.mocked(getOverriddenConfig).mockReturnValue(config);
|
||||
|
||||
manager.setConfig(config);
|
||||
expect(api.getStyleManager().setMinMaxHeight).toBeCalled();
|
||||
expect(api.getStyleManager().updateFromConfig).toBeCalled();
|
||||
|
||||
vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear();
|
||||
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
|
||||
manager.computeOverrideConfig();
|
||||
|
||||
expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled();
|
||||
expect(api.getStyleManager().updateFromConfig).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should override', () => {
|
||||
@@ -204,7 +204,7 @@ describe('ConfigManager', () => {
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
};
|
||||
manager.setConfig(config_1);
|
||||
vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear();
|
||||
vi.mocked(api.getStyleManager().updateFromConfig).mockClear();
|
||||
|
||||
const config_2 = {
|
||||
type: 'custom:frigate-card',
|
||||
@@ -213,7 +213,7 @@ describe('ConfigManager', () => {
|
||||
vi.mocked(getOverriddenConfig).mockReturnValue(config_2);
|
||||
manager.computeOverrideConfig();
|
||||
|
||||
expect(api.getStyleManager().setMinMaxHeight).toBeCalled();
|
||||
expect(api.getStyleManager().updateFromConfig).toBeCalled();
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig());
|
||||
});
|
||||
|
||||
@@ -38,13 +38,13 @@ describe('HASSManager', () => {
|
||||
expect(manager.hasHASS()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set light or dark mode upon setting hass', () => {
|
||||
it('should update theme upon setting hass', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new HASSManager(api);
|
||||
|
||||
manager.setHASS(createHASS());
|
||||
|
||||
expect(api.getStyleManager().setLightOrDarkMode).toBeCalled();
|
||||
expect(api.getStyleManager().applyTheme).toBeCalled();
|
||||
});
|
||||
|
||||
describe('should set condition manager state', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { add } from 'date-fns';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { InteractionManager } from '../../src/card-controller/interaction-manager';
|
||||
import { createCardAPI, createConfig } from '../test-utils';
|
||||
import { createCardAPI, createConfig, createLitElement } from '../test-utils';
|
||||
|
||||
vi.mock('lodash-es/throttle', () => ({
|
||||
default: vi.fn((fn) => fn),
|
||||
@@ -17,14 +17,20 @@ describe('InteractionManager', () => {
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const element = createLitElement();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const manager = new InteractionManager(api);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ interaction: false });
|
||||
expect(element.getAttribute('interaction')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not take action without an interaction timeout', () => {
|
||||
it('should still report interaction without an interaction timeout', () => {
|
||||
const api = createCardAPI();
|
||||
const element = createLitElement();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
@@ -36,11 +42,15 @@ describe('InteractionManager', () => {
|
||||
|
||||
manager.reportInteraction();
|
||||
|
||||
expect(manager.hasInteraction()).toBeFalsy();
|
||||
expect(element.getAttribute('interaction')).not.toBeNull();
|
||||
expect(manager.hasInteraction()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set condition state', () => {
|
||||
const api = createCardAPI();
|
||||
const element = createLitElement();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
@@ -55,12 +65,14 @@ describe('InteractionManager', () => {
|
||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
||||
|
||||
manager.reportInteraction();
|
||||
|
||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
interaction: true,
|
||||
}),
|
||||
);
|
||||
expect(manager.hasInteraction()).toBeTruthy();
|
||||
expect(element.getAttribute('interaction')).not.toBeNull();
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
@@ -71,5 +83,6 @@ describe('InteractionManager', () => {
|
||||
}),
|
||||
);
|
||||
expect(manager.hasInteraction()).toBeFalsy();
|
||||
expect(element.getAttribute('interaction')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { StyleManager } from '../../src/card-controller/style-manager';
|
||||
import { FrigateCardView } from '../../src/config/types';
|
||||
import { createCardAPI, createConfig, createHASS, createView } from '../test-utils';
|
||||
import { FrigateCardView, ThemeName } from '../../src/config/types';
|
||||
import { createCardAPI, createConfig, createView } from '../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('StyleManager', () => {
|
||||
@@ -9,97 +9,45 @@ describe('StyleManager', () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('setLightOrDarkMode', () => {
|
||||
it('dark mode unspecified', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setLightOrDarkMode();
|
||||
|
||||
expect(element.getAttribute('dark')).toBeNull();
|
||||
});
|
||||
|
||||
it('dark mode explicitly off', () => {
|
||||
describe('should set dimmable', () => {
|
||||
it('should be dimmable when dim is true', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
dark_mode: 'off',
|
||||
dim: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setLightOrDarkMode();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(element.getAttribute('dark')).toBeNull();
|
||||
expect(element.getAttribute('dimmable')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('dark mode explicitly set', () => {
|
||||
it('should not be dimmable when dim is false', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
dark_mode: 'on',
|
||||
dim: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setLightOrDarkMode();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(element.getAttribute('dark')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('dark mode auto without interaction', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
dark_mode: 'auto',
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setLightOrDarkMode();
|
||||
|
||||
expect(element.getAttribute('dark')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('dark mode auto with HA dark mode', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
dark_mode: 'auto',
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
|
||||
const hass = createHASS();
|
||||
hass.themes.darkMode = true;
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setLightOrDarkMode();
|
||||
|
||||
expect(element.getAttribute('dark')).not.toBeNull();
|
||||
expect(element.getAttribute('dimmable')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setExpandedMode', () => {
|
||||
describe('should set expanded mode', () => {
|
||||
it('with no view or known media', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
@@ -189,14 +137,14 @@ describe('StyleManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMinMaxHeight', () => {
|
||||
describe('should set min and max height', () => {
|
||||
it('without a config', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setMinMaxHeight();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(element.style.getPropertyValue('--frigate-card-max-height')).toBeFalsy();
|
||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBeFalsy();
|
||||
@@ -215,13 +163,13 @@ describe('StyleManager', () => {
|
||||
);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setMinMaxHeight();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(element.style.getPropertyValue('--frigate-card-height')).toBe('800px');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setPerformance', () => {
|
||||
describe('should set performance', () => {
|
||||
it('no styles set', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
@@ -229,7 +177,7 @@ describe('StyleManager', () => {
|
||||
vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue({});
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setPerformance();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(
|
||||
element.style.getPropertyValue('--frigate-card-css-box-shadow'),
|
||||
@@ -255,7 +203,7 @@ describe('StyleManager', () => {
|
||||
);
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.setPerformance();
|
||||
manager.updateFromConfig();
|
||||
|
||||
expect(element.style.getPropertyValue('--frigate-card-css-box-shadow')).toEqual(
|
||||
'none',
|
||||
@@ -411,4 +359,111 @@ describe('StyleManager', () => {
|
||||
expect(manager.getAspectRatioStyle()).toEqual({ 'aspect-ratio': '4 / 3' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should apply themes', () => {
|
||||
it('should not apply themes without a config', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.applyTheme();
|
||||
|
||||
expect(element.getAttribute('themes')).toBeNull();
|
||||
});
|
||||
|
||||
describe('should apply named theme', () => {
|
||||
it.each([
|
||||
['light' as const],
|
||||
['dark' as const],
|
||||
['traditional' as const],
|
||||
['ha' as const],
|
||||
])('%s', (theme: ThemeName) => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
theme: {
|
||||
themes: [theme],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.applyTheme();
|
||||
|
||||
expect(element.getAttribute('themes')).toBe(theme);
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply multiple themes', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
theme: {
|
||||
themes: ['light', 'traditional'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.applyTheme();
|
||||
|
||||
expect(element.getAttribute('themes')).toBe('light traditional');
|
||||
});
|
||||
|
||||
it('should treat empty themes list as default', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
theme: {
|
||||
themes: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.applyTheme();
|
||||
|
||||
expect(element.getAttribute('themes')).toBe('traditional');
|
||||
});
|
||||
|
||||
it('should apply overrides', () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
theme: {
|
||||
overrides: {
|
||||
'--test-key': 'test-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const manager = new StyleManager(api);
|
||||
|
||||
manager.applyTheme();
|
||||
|
||||
expect(element.style.getPropertyValue('--test-key')).toBe('test-value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,10 @@ describe('IconController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return iris SVG for iris icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'iris' })).toMatch(/iris.svg$/);
|
||||
});
|
||||
|
||||
it('should return null for mdi icon', () => {
|
||||
expect(new IconController().getCustomIcon({ icon: 'mdi:car' })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('MenuButtonController', () => {
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:video-input-component',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
title: 'Substream(s)',
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
@@ -442,7 +442,7 @@ describe('MenuButtonController', () => {
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:video-input-component',
|
||||
title: 'Substream(s)',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
enabled: true,
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-submenu',
|
||||
@@ -508,7 +508,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Live view',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'live' },
|
||||
});
|
||||
});
|
||||
@@ -562,7 +562,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Clips gallery',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'clips' },
|
||||
hold_action: { action: 'fire-dom-event', frigate_card_action: 'clip' },
|
||||
});
|
||||
@@ -615,7 +615,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Snapshots gallery',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'snapshots' },
|
||||
hold_action: { action: 'fire-dom-event', frigate_card_action: 'snapshot' },
|
||||
});
|
||||
@@ -670,7 +670,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Recordings gallery',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'recordings' },
|
||||
hold_action: { action: 'fire-dom-event', frigate_card_action: 'recording' },
|
||||
});
|
||||
@@ -726,7 +726,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Static image',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'image' },
|
||||
});
|
||||
});
|
||||
@@ -781,7 +781,7 @@ describe('MenuButtonController', () => {
|
||||
priority: 50,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Timeline view',
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'timeline' },
|
||||
});
|
||||
});
|
||||
@@ -938,7 +938,7 @@ describe('MenuButtonController', () => {
|
||||
title: 'Microphone',
|
||||
style: {
|
||||
animation: 'pulse 3s infinite',
|
||||
color: 'var(--error-color, white)',
|
||||
color: 'var(--frigate-card-menu-button-critical-color)',
|
||||
},
|
||||
start_tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
@@ -1110,7 +1110,7 @@ describe('MenuButtonController', () => {
|
||||
title: 'Microphone',
|
||||
style: {
|
||||
animation: 'pulse 3s infinite',
|
||||
color: 'var(--error-color, white)',
|
||||
color: 'var(--frigate-card-menu-button-critical-color)',
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
@@ -1148,7 +1148,7 @@ describe('MenuButtonController', () => {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Fullscreen',
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1178,7 +1178,7 @@ describe('MenuButtonController', () => {
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: 'Expand',
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'expand' },
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1413,7 +1413,10 @@ describe('MenuButtonController', () => {
|
||||
displayMode === 'grid'
|
||||
? 'Show single media viewer'
|
||||
: 'Show media viewer for each camera in a grid',
|
||||
style: displayMode === 'grid' ? { color: 'var(--primary-color, white)' } : {},
|
||||
style:
|
||||
displayMode === 'grid'
|
||||
? { color: 'var(--frigate-card-menu-button-active-color)' }
|
||||
: {},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
frigate_card_action: 'display_mode_select',
|
||||
@@ -1481,7 +1484,7 @@ describe('MenuButtonController', () => {
|
||||
icon: 'mdi:pan',
|
||||
priority: 50,
|
||||
style: {
|
||||
color: 'var(--primary-color, white)',
|
||||
color: 'var(--frigate-card-menu-button-active-color)',
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
@@ -1554,7 +1557,7 @@ describe('MenuButtonController', () => {
|
||||
icon: 'mdi:pan',
|
||||
priority: 50,
|
||||
style: {
|
||||
color: 'var(--primary-color, white)',
|
||||
color: 'var(--frigate-card-menu-button-active-color)',
|
||||
},
|
||||
tap_action: {
|
||||
action: 'fire-dom-event',
|
||||
@@ -1701,7 +1704,7 @@ describe('MenuButtonController', () => {
|
||||
|
||||
expect(calculateButtons(controller, { view: view })).toContainEqual({
|
||||
...button,
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1714,7 +1717,7 @@ describe('MenuButtonController', () => {
|
||||
|
||||
expect(calculateButtons(controller)).toContainEqual({
|
||||
...button,
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1727,7 +1730,7 @@ describe('MenuButtonController', () => {
|
||||
|
||||
expect(calculateButtons(controller, { inFullscreenMode: true })).toContainEqual({
|
||||
...button,
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1745,7 +1748,7 @@ describe('MenuButtonController', () => {
|
||||
|
||||
expect(calculateButtons(controller, { view: view })).toContainEqual({
|
||||
...button,
|
||||
style: { color: 'var(--primary-color, white)' },
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ describe('StatusBarController', () => {
|
||||
it('should set config', () => {
|
||||
const host = createLitElement();
|
||||
const controller = new StatusBarController(host);
|
||||
const config = createConfig({
|
||||
position: 'top',
|
||||
style: 'hover',
|
||||
height: 50,
|
||||
});
|
||||
controller.setConfig(config);
|
||||
|
||||
controller.setConfig(
|
||||
createConfig({
|
||||
position: 'top',
|
||||
style: 'hover',
|
||||
height: 50,
|
||||
}),
|
||||
);
|
||||
expect(controller.getConfig()).toEqual(config);
|
||||
expect(host.style.getPropertyValue('--frigate-card-status-bar-height')).toBe(
|
||||
'50px',
|
||||
);
|
||||
|
||||
@@ -3406,4 +3406,31 @@ describe('should handle version specific upgrades', () => {
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('v6.1.2+', () => {
|
||||
describe('view.dark_mode -> view.dim', () => {
|
||||
it.each([
|
||||
['on' as const, true],
|
||||
['auto' as const, false],
|
||||
['off' as const, false],
|
||||
])('%s', (darkMode: 'on' | 'off' | 'auto', expected: boolean) => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
view: {
|
||||
dark_mode: darkMode,
|
||||
},
|
||||
};
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [{ camera_entity: 'camera.office' }],
|
||||
view: {
|
||||
dim: expected,
|
||||
},
|
||||
});
|
||||
postUpgradeChecks(config);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,7 +285,7 @@ describe('config defaults', () => {
|
||||
},
|
||||
},
|
||||
status_bar: {
|
||||
height: 30,
|
||||
height: 40,
|
||||
items: {
|
||||
engine: {
|
||||
enabled: true,
|
||||
@@ -329,7 +329,6 @@ describe('config defaults', () => {
|
||||
type: 'frigate-hass-card',
|
||||
view: {
|
||||
camera_select: 'current',
|
||||
dark_mode: 'off',
|
||||
default: 'live',
|
||||
keyboard_shortcuts: {
|
||||
enabled: true,
|
||||
@@ -355,6 +354,9 @@ describe('config defaults', () => {
|
||||
key: '-',
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
themes: ['traditional'],
|
||||
},
|
||||
triggers: {
|
||||
show_trigger_status: false,
|
||||
untrigger_seconds: 0,
|
||||
@@ -366,6 +368,7 @@ describe('config defaults', () => {
|
||||
filter_selected_camera: true,
|
||||
},
|
||||
interaction_seconds: 300,
|
||||
dim: false,
|
||||
default_cycle_camera: false,
|
||||
default_reset: {
|
||||
after_interaction: false,
|
||||
|
||||
@@ -3693,6 +3693,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^4.1.3":
|
||||
version: 4.3.0
|
||||
resolution: "css-select@npm:4.3.0"
|
||||
dependencies:
|
||||
boolbase: "npm:^1.0.0"
|
||||
css-what: "npm:^6.0.1"
|
||||
domhandler: "npm:^4.3.1"
|
||||
domutils: "npm:^2.8.0"
|
||||
nth-check: "npm:^2.0.1"
|
||||
checksum: 10c0/a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-select@npm:^5.1.0":
|
||||
version: 5.1.0
|
||||
resolution: "css-select@npm:5.1.0"
|
||||
@@ -3706,6 +3719,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-tree@npm:^1.1.2, css-tree@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "css-tree@npm:1.1.3"
|
||||
dependencies:
|
||||
mdn-data: "npm:2.0.14"
|
||||
source-map: "npm:^0.6.1"
|
||||
checksum: 10c0/499a507bfa39b8b2128f49736882c0dd636b0cd3370f2c69f4558ec86d269113286b7df469afc955de6a68b0dba00bc533e40022a73698081d600072d5d83c1c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-tree@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "css-tree@npm:2.3.1"
|
||||
@@ -3726,7 +3749,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"css-what@npm:^6.1.0":
|
||||
"css-what@npm:^6.0.1, css-what@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "css-what@npm:6.1.0"
|
||||
checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746
|
||||
@@ -3810,6 +3833,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csso@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "csso@npm:4.2.0"
|
||||
dependencies:
|
||||
css-tree: "npm:^1.1.2"
|
||||
checksum: 10c0/f8c6b1300efaa0f8855a7905ae3794a29c6496e7f16a71dec31eb6ca7cfb1f058a4b03fd39b66c4deac6cb06bf6b4ba86da7b67d7320389cb9994d52b924b903
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csso@npm:^5.0.5":
|
||||
version: 5.0.5
|
||||
resolution: "csso@npm:5.0.5"
|
||||
@@ -4134,6 +4166,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-serializer@npm:^1.0.1":
|
||||
version: 1.4.1
|
||||
resolution: "dom-serializer@npm:1.4.1"
|
||||
dependencies:
|
||||
domelementtype: "npm:^2.0.1"
|
||||
domhandler: "npm:^4.2.0"
|
||||
entities: "npm:^2.0.0"
|
||||
checksum: 10c0/67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-serializer@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "dom-serializer@npm:2.0.0"
|
||||
@@ -4145,7 +4188,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domelementtype@npm:^2.3.0":
|
||||
"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "domelementtype@npm:2.3.0"
|
||||
checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9
|
||||
@@ -4161,6 +4204,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domhandler@npm:^4.2.0, domhandler@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "domhandler@npm:4.3.1"
|
||||
dependencies:
|
||||
domelementtype: "npm:^2.2.0"
|
||||
checksum: 10c0/5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3":
|
||||
version: 5.0.3
|
||||
resolution: "domhandler@npm:5.0.3"
|
||||
@@ -4170,6 +4222,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domutils@npm:^2.8.0":
|
||||
version: 2.8.0
|
||||
resolution: "domutils@npm:2.8.0"
|
||||
dependencies:
|
||||
dom-serializer: "npm:^1.0.1"
|
||||
domelementtype: "npm:^2.2.0"
|
||||
domhandler: "npm:^4.2.0"
|
||||
checksum: 10c0/d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domutils@npm:^3.0.1":
|
||||
version: 3.1.0
|
||||
resolution: "domutils@npm:3.1.0"
|
||||
@@ -4315,6 +4378,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^2.0.0":
|
||||
version: 2.2.0
|
||||
resolution: "entities@npm:2.2.0"
|
||||
checksum: 10c0/7fba6af1f116300d2ba1c5673fc218af1961b20908638391b4e1e6d5850314ee2ac3ec22d741b3a8060479911c99305164aed19b6254bde75e7e6b1b2c3f3aa3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^4.2.0, entities@npm:^4.4.0":
|
||||
version: 4.5.0
|
||||
resolution: "entities@npm:4.5.0"
|
||||
@@ -5182,6 +5252,7 @@ __metadata:
|
||||
rollup-plugin-git-info: "npm:^1.0.0"
|
||||
rollup-plugin-serve: "npm:^1.1.1"
|
||||
rollup-plugin-styler: "npm:^1.8.0"
|
||||
rollup-plugin-svgo: "npm:^2.0.0"
|
||||
rollup-plugin-visualizer: "npm:^5.12.0"
|
||||
sass: "patch:sass@npm%3A1.77.4#~/.yarn/patches/sass-npm-1.77.4-13b6910aea.patch"
|
||||
screenfull: "npm:^6.0.2"
|
||||
@@ -7432,6 +7503,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdn-data@npm:2.0.14":
|
||||
version: 2.0.14
|
||||
resolution: "mdn-data@npm:2.0.14"
|
||||
checksum: 10c0/67241f8708c1e665a061d2b042d2d243366e93e5bf1f917693007f6d55111588b952dcbfd3ea9c2d0969fb754aad81b30fdcfdcc24546495fc3b24336b28d4bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdn-data@npm:2.0.28":
|
||||
version: 2.0.28
|
||||
resolution: "mdn-data@npm:2.0.28"
|
||||
@@ -9826,6 +9904,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup-plugin-svgo@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "rollup-plugin-svgo@npm:2.0.0"
|
||||
dependencies:
|
||||
svgo: "npm:2.8.0"
|
||||
checksum: 10c0/3dead444692eca0c99da8ed668e9a835ceaf6a1e0c6eaf060ae0b113f3f227127d2efb2ba657f910bc51c5733b92a2181bdc393dbf6b905da19c2e37997a8fa3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup-plugin-visualizer@npm:^5.12.0":
|
||||
version: 5.12.0
|
||||
resolution: "rollup-plugin-visualizer@npm:5.12.0"
|
||||
@@ -10511,6 +10598,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stable@npm:^0.1.8":
|
||||
version: 0.1.8
|
||||
resolution: "stable@npm:0.1.8"
|
||||
checksum: 10c0/df74b5883075076e78f8e365e4068ecd977af6c09da510cfc3148a303d4b87bc9aa8f7c48feb67ed4ef970b6140bd9eabba2129e28024aa88df5ea0114cba39d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stackback@npm:0.0.2":
|
||||
version: 0.0.2
|
||||
resolution: "stackback@npm:0.0.2"
|
||||
@@ -10771,6 +10865,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"svgo@npm:2.8.0":
|
||||
version: 2.8.0
|
||||
resolution: "svgo@npm:2.8.0"
|
||||
dependencies:
|
||||
"@trysound/sax": "npm:0.2.0"
|
||||
commander: "npm:^7.2.0"
|
||||
css-select: "npm:^4.1.3"
|
||||
css-tree: "npm:^1.1.3"
|
||||
csso: "npm:^4.2.0"
|
||||
picocolors: "npm:^1.0.0"
|
||||
stable: "npm:^0.1.8"
|
||||
bin:
|
||||
svgo: bin/svgo
|
||||
checksum: 10c0/0741f5d5cad63111a90a0ce7a1a5a9013f6d293e871b75efe39addb57f29a263e45294e485a4d2ff9cc260a5d142c8b5937b2234b4ef05efdd2706fb2d360ecc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"svgo@npm:^3.2.0":
|
||||
version: 3.3.2
|
||||
resolution: "svgo@npm:3.3.2"
|
||||
|
||||
Reference in New Issue
Block a user