Initial version of status bar.

This commit is contained in:
Dermot Duffy
2024-08-16 20:08:21 -07:00
parent db9303c60d
commit e503f0e429
77 changed files with 3072 additions and 1061 deletions
@@ -0,0 +1,24 @@
import { StatusBarActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { FrigateCardAction } from './base';
export class StatusBarAction extends FrigateCardAction<StatusBarActionConfig> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async execute(api: CardActionsAPI): Promise<void> {
switch (this._action.status_bar_action) {
case 'reset':
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
break;
case 'add':
this._action.items?.forEach((item) =>
api.getStatusBarItemManager().addDynamicStatusBarItem(item),
);
break;
case 'remove':
this._action.items?.forEach((item) =>
api.getStatusBarItemManager().removeDynamicStatusBarItem(item),
);
break;
}
}
}
+4 -1
View File
@@ -19,11 +19,12 @@ import { MuteAction } from './actions/mute';
import { PauseAction } from './actions/pause';
import { PlayAction } from './actions/play';
import { PTZAction } from './actions/ptz';
import { PTZControlsAction } from './actions/ptz-controls';
import { PTZDigitalAction } from './actions/ptz-digital';
import { PTZMultiAction } from './actions/ptz-multi';
import { ScreenshotAction } from './actions/screenshot';
import { PTZControlsAction } from './actions/ptz-controls';
import { SleepAction } from './actions/sleep';
import { StatusBarAction } from './actions/status-bar';
import { SubstreamOffAction } from './actions/substream-off';
import { SubstreamOnAction } from './actions/substream-on';
import { SubstreamSelectAction } from './actions/substream-select';
@@ -121,6 +122,8 @@ export class ActionFactory {
return new PTZControlsAction(context, frigateCardAction, options?.config);
case 'log':
return new LogAction(context, frigateCardAction, options?.config);
case 'status_bar':
return new StatusBarAction(context, frigateCardAction, options?.config);
}
/* istanbul ignore next: this path cannot be reached -- @preserve */
@@ -96,6 +96,8 @@ export class ConfigManager {
this._api.getMessageManager().reset();
this._api.getStyleManager().setPerformance();
this._api.getCardElementManager().update();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
setKeyboardShortcutsFromConfig(this._api, this);
setAutomationsFromConfig(this._api);
+6
View File
@@ -28,6 +28,7 @@ import { MediaPlayerManager } from './media-player-manager';
import { MessageManager } from './message-manager';
import { MicrophoneManager } from './microphone-manager';
import { QueryStringManager } from './query-string-manager';
import { StatusBarItemManager } from './status-bar-item-manager';
import { StyleManager } from './style-manager';
import { TriggersManager } from './triggers-manager';
import {
@@ -110,6 +111,7 @@ export class CardController
protected _messageManager = new MessageManager(this);
protected _microphoneManager = new MicrophoneManager(this);
protected _queryStringManager = new QueryStringManager(this);
protected _statusBarItemManager = new StatusBarItemManager(this);
protected _styleManager = new StyleManager(this);
protected _triggersManager = new TriggersManager(this);
protected _viewManager = new ViewManager(this);
@@ -228,6 +230,10 @@ export class CardController
return this._resolvedMediaCache;
}
public getStatusBarItemManager(): StatusBarItemManager {
return this._statusBarItemManager;
}
public static getStubConfig(entities: string[]): FrigateCardConfig {
const cameraEntity = entities.find((element) => element.startsWith('camera.'));
return {
@@ -0,0 +1,160 @@
import { isEqual } from 'lodash-es';
import { CameraManager } from '../camera-manager/manager';
import { StatusBarConfig, StatusBarItem } from '../config/types';
import { MediaLoadedInfo } from '../types';
import { View } from '../view/view';
import { CardStatusBarAPI } from './types';
const RESOLUTION_TOLERANCE_PCT = 0.01;
export class StatusBarItemManager {
protected _api: CardStatusBarAPI;
constructor(api: CardStatusBarAPI) {
this._api = api;
}
protected _items: StatusBarItem[] = [];
protected _dynamicItems: StatusBarItem[] = [];
public addDynamicStatusBarItem(item: StatusBarItem): void {
if (!this._dynamicItems.includes(item)) {
this._dynamicItems.push(item);
}
this._api.getCardElementManager().update();
}
public removeDynamicStatusBarItem(item: StatusBarItem): void {
this._dynamicItems = this._dynamicItems.filter(
(existingItem) => !isEqual(existingItem, item),
);
this._api.getCardElementManager().update();
}
public removeAllDynamicStatusBarItems(): void {
this._dynamicItems = [];
this._api.getCardElementManager().update();
}
public calculateItems(options?: {
statusConfig?: StatusBarConfig | null;
cameraManager?: CameraManager | null;
view?: View | null;
mediaLoadedInfo?: MediaLoadedInfo | null;
}): StatusBarItem[] {
const cameraMetadata = options?.view
? options?.cameraManager?.getCameraMetadata(options?.view?.camera)
: null;
const engineLogoIcon = cameraMetadata?.engineLogo ?? null;
const title = options?.view?.is('live')
? cameraMetadata?.title ?? null
: options?.view?.isAnyMediaView()
? options?.view.queryResults?.getSelectedResult()?.getTitle() ?? null
: null;
const resolution = options?.mediaLoadedInfo
? this._calculateResolution(options?.mediaLoadedInfo)
: null;
const technology = options?.mediaLoadedInfo?.technology?.length
? options?.mediaLoadedInfo.technology[0]
: null;
return [
...(title
? [
{
type: 'custom:frigate-card-status-bar-string' as const,
string: title,
expand: true,
sufficient: true,
...options?.statusConfig?.items.title,
},
]
: []),
...(resolution
? [
{
type: 'custom:frigate-card-status-bar-string' as const,
string: resolution,
...options?.statusConfig?.items.resolution,
},
]
: []),
...(technology && technology === 'webrtc'
? [
{
type: 'custom:frigate-card-status-bar-icon' as const,
icon: 'mdi:webrtc',
...options?.statusConfig?.items.technology,
},
]
: !!technology
? [
{
type: 'custom:frigate-card-status-bar-string' as const,
string: technology.toUpperCase(),
...options?.statusConfig?.items.technology,
},
]
: []),
...(engineLogoIcon
? [
{
type: 'custom:frigate-card-status-bar-image' as const,
image: engineLogoIcon,
...options?.statusConfig?.items.engine,
},
]
: []),
...this._dynamicItems,
];
}
protected _matchesWidthHeight(
mediaLoadedInfo: MediaLoadedInfo | null,
width: number,
height: number,
): boolean {
const widthMin = width * (1 - RESOLUTION_TOLERANCE_PCT);
const widthMax = width * (1 + RESOLUTION_TOLERANCE_PCT);
const heightMin = height * (1 - RESOLUTION_TOLERANCE_PCT);
const heightMax = height * (1 + RESOLUTION_TOLERANCE_PCT);
const matchesDimension = (val: number, min: number, max: number): boolean => {
return val >= min && val <= max;
};
// Allows matching the resolution width and height in either orientation,
// and within RESOLUTION_TOLERANCE_PCT of the resolution.
return (
!!mediaLoadedInfo &&
((matchesDimension(mediaLoadedInfo.width, widthMin, widthMax) &&
matchesDimension(mediaLoadedInfo.height, heightMin, heightMax)) ||
(matchesDimension(mediaLoadedInfo.height, widthMin, widthMax) &&
matchesDimension(mediaLoadedInfo.width, heightMin, heightMax)))
);
}
protected _calculateResolution(mediaLoadedInfo: MediaLoadedInfo): string {
// Ordered roughly by a guess at most common towards the top.
if (this._matchesWidthHeight(mediaLoadedInfo, 1920, 1080)) {
return '1080p';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 1280, 720)) {
return '720p';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 640, 480)) {
return 'VGA';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 3840, 2160)) {
return '4K';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 720, 480)) {
return '480p';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 720, 576)) {
return '576p';
} else if (this._matchesWidthHeight(mediaLoadedInfo, 7680, 4320)) {
return '8K';
} else {
return `${mediaLoadedInfo.width}x${mediaLoadedInfo.height}`;
}
}
}
+12 -5
View File
@@ -1,29 +1,30 @@
import type { CameraManager } from '../camera-manager/manager';
import type { ConditionsManager } from './conditions-manager';
import type { Automation } from '../config/types';
import type { EntityRegistryManager } from '../utils/ha/entity-registry';
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
import type { ActionsManager } from './actions/actions-manager';
import type { DefaultManager } from './default-manager';
import type { AutomationsManager } from './automations-manager';
import type { CameraURLManager } from './camera-url-manager';
import type { CardElementManager } from './card-element-manager';
import type { ConditionsManager } from './conditions-manager';
import type { ConfigManager } from './config/config-manager';
import type { DefaultManager } from './default-manager';
import type { DownloadManager } from './download-manager';
import type { ExpandManager } from './expand-manager';
import type { FullscreenManager } from './fullscreen-manager';
import type { HASSManager } from './hass-manager';
import type { InitializationManager } from './initialization-manager';
import type { InteractionManager } from './interaction-manager';
import type { KeyboardStateManager } from './keyboard-state-manager';
import type { MediaLoadedInfoManager } from './media-info-manager';
import type { MediaPlayerManager } from './media-player-manager';
import type { MessageManager } from './message-manager';
import type { MicrophoneManager } from './microphone-manager';
import type { QueryStringManager } from './query-string-manager';
import type { StatusBarItemManager } from './status-bar-item-manager';
import type { StyleManager } from './style-manager';
import type { TriggersManager } from './triggers-manager';
import type { ViewManager } from './view/view-manager';
import type { QueryStringManager } from './query-string-manager';
import { KeyboardStateManager } from './keyboard-state-manager';
import { Automation } from '../config/types';
// *************************************************************************
// Manager APIs
@@ -47,6 +48,7 @@ export interface CardActionsAPI {
getMediaPlayerManager(): MediaPlayerManager;
getMessageManager(): MessageManager;
getMicrophoneManager(): MicrophoneManager;
getStatusBarItemManager(): StatusBarItemManager;
getTriggersManager(): TriggersManager;
getViewManager(): ViewManager;
}
@@ -89,6 +91,7 @@ export interface CardConfigAPI {
getInitializationManager(): InitializationManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMessageManager(): MessageManager;
getStatusBarItemManager(): StatusBarItemManager;
getStyleManager(): StyleManager;
getViewManager(): ViewManager;
}
@@ -218,6 +221,10 @@ export interface CardQueryStringAPI {
getViewManager(): ViewManager;
}
export interface CardStatusBarAPI {
getCardElementManager(): CardElementManager;
}
export interface CardStyleAPI {
getCardElementManager(): CardElementManager;
getConfigManager(): ConfigManager;
+104 -8
View File
@@ -2,6 +2,7 @@ import { HomeAssistant, LovelaceCardEditor } from '@dermotduffy/custom-card-help
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import 'web-dialog';
@@ -16,10 +17,18 @@ import './components/menu.js';
import { FrigateCardMenu } from './components/menu.js';
import './components/message.js';
import { renderMessage, renderProgressIndicator } from './components/message.js';
import './components/overlay.js';
import { FrigateCardOverlay } from './components/overlay.js';
import './components/status-bar';
import './components/thumbnail-carousel.js';
import './components/views.js';
import { FrigateCardViews } from './components/views.js';
import { FrigateCardConfig, MenuItem, RawFrigateCardConfig } from './config/types';
import {
FrigateCardConfig,
MenuItem,
RawFrigateCardConfig,
StatusBarItem,
} from './config/types';
import { REPO_URL } from './const.js';
import { localize } from './localize/localize.js';
import cardStyle from './scss/card.scss';
@@ -103,6 +112,7 @@ class FrigateCard extends LitElement {
protected _menuButtonController = new MenuButtonController();
protected _refMenu: Ref<FrigateCardMenu> = createRef();
protected _refOverlay: Ref<FrigateCardOverlay> = createRef();
protected _refMain: Ref<HTMLElement> = createRef();
protected _refElements: Ref<FrigateCardElements> = createRef();
protected _refViews: Ref<FrigateCardViews> = createRef();
@@ -185,7 +195,64 @@ class FrigateCard extends LitElement {
this._controller.getInitializationManager().initializeBackgroundIfNecessary();
}
protected _renderMenu(): TemplateResult | void {
protected _renderMenuStatusContainer(
position: 'top' | 'bottom' | 'overlay',
): TemplateResult | void {
if (!this._config) {
return;
}
const menuStyle = this._config.menu.style;
const menuPosition = this._config.menu.position;
const statusBarStyle = this._config.status_bar.style;
const statusBarPosition = this._config.status_bar.position;
if (
// If there's nothing to render...
(menuStyle === 'none' && statusBarStyle === 'none') ||
// ... or the position I'm rendering does not contain the menu/status bar
(position !== 'overlay' &&
menuPosition !== position &&
statusBarPosition !== position)
) {
// ... then there's nothing to do.
return;
}
const getContents = (kind: 'overlay' | 'outerlay'): TemplateResult => {
const shouldRenderMenu =
menuStyle !== 'none' &&
((menuStyle === 'outside' && kind === 'outerlay') ||
(menuStyle !== 'outside' && kind === 'overlay'));
const shouldRenderStatusBar =
statusBarStyle !== 'none' &&
((statusBarStyle === 'outside' && kind === 'outerlay') ||
(statusBarStyle !== 'outside' && kind === 'overlay'));
// As an exception, to improve the visual flow, if the menu is being
// rendered on the bottom, render the status bar first.
return html`
${shouldRenderMenu && menuPosition !== 'bottom'
? this._renderMenu(menuPosition)
: ''}
${shouldRenderStatusBar ? this._renderStatusBar(statusBarPosition) : ''}
${shouldRenderMenu && menuPosition === 'bottom'
? this._renderMenu(menuPosition)
: ''}
`;
};
return html`
${position === 'overlay'
? html`<frigate-card-overlay>${getContents('overlay')}</frigate-card-overlay>`
: html`<div class="outerlay" data-position="${position}">
${getContents('outerlay')}
</div>`}
`;
}
protected _renderMenu(slot?: string): TemplateResult | void {
const view = this._controller.getViewManager().getView();
if (!this._hass || !this._config || !view) {
return;
@@ -193,6 +260,7 @@ class FrigateCard extends LitElement {
return html`
<frigate-card-menu
${ref(this._refMenu)}
slot=${ifDefined(slot)}
.hass=${this._hass}
.menuConfig=${this._config.menu}
.buttons=${this._menuButtonController.calculateButtons(
@@ -215,6 +283,25 @@ class FrigateCard extends LitElement {
`;
}
protected _renderStatusBar(slot?: string): TemplateResult | void {
if (!this._config) {
return;
}
return html`
<frigate-card-status-bar
slot=${ifDefined(slot)}
.items=${this._controller.getStatusBarItemManager().calculateItems({
statusConfig: this._config.status_bar,
cameraManager: this._controller.getCameraManager(),
view: this._controller.getViewManager().getView(),
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
})}
.config=${this._config.status_bar}
></frigate-card-status-bar>
`;
}
protected firstUpdated(): void {
// Execute query string actions after first render is complete.
this._controller.getQueryStringManager().executeNonViewRelated();
@@ -250,8 +337,6 @@ class FrigateCard extends LitElement {
};
const actions = this._controller.getActionsManager().getMergedActions();
const renderMenuAbove =
this._config?.menu.style === 'outside' && this._config?.menu.position === 'top';
const cameraManager = this._controller.getCameraManager();
// Caution: Keep the main div and the menu next to one another in order to
@@ -281,8 +366,9 @@ class FrigateCard extends LitElement {
}
@frigate-card:focus=${() => this.focus()}
>
${renderMenuAbove ? this._renderMenu() : ''}
${this._renderMenuStatusContainer('top')}
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
${this._renderMenuStatusContainer('overlay')}
${!cameraManager.isInitialized() &&
!this._controller.getMessageManager().hasMessage()
? renderProgressIndicator({
@@ -320,7 +406,7 @@ class FrigateCard extends LitElement {
renderMessage(this._controller.getMessageManager().getMessage())
}
</div>
${!renderMenuAbove ? this._renderMenu() : ''}
${this._renderMenuStatusContainer('bottom')}
${this._config?.elements
? // Elements need to render after the main views so it can render 'on
// top'.
@@ -331,14 +417,24 @@ class FrigateCard extends LitElement {
.conditionsManagerEpoch=${this._controller
.getConditionsManager()
?.getEpoch()}
@frigate-card:menu-add=${(ev: CustomEvent<MenuItem>) => {
@frigate-card:menu:add=${(ev: CustomEvent<MenuItem>) => {
this._menuButtonController.addDynamicMenuButton(ev.detail);
this.requestUpdate();
}}
@frigate-card:menu-remove=${(ev: CustomEvent<MenuItem>) => {
@frigate-card:menu:remove=${(ev: CustomEvent<MenuItem>) => {
this._menuButtonController.removeDynamicMenuButton(ev.detail);
this.requestUpdate();
}}
@frigate-card:status-bar:add=${(ev: CustomEvent<StatusBarItem>) => {
this._controller
.getStatusBarItemManager()
.addDynamicStatusBarItem(ev.detail);
}}
@frigate-card:status-bar:remove=${(ev: CustomEvent<StatusBarItem>) => {
this._controller
.getStatusBarItemManager()
.removeDynamicStatusBarItem(ev.detail);
}}
@frigate-card:conditions:evaluate=${(
ev: ConditionsEvaluateRequestEvent,
) => {
@@ -0,0 +1,12 @@
import { MediaTechnology } from '../../../types';
import { VideoRTC } from '../../../components/live/go2rtc/video-rtc';
export const getTechnologyForVideoRTC = (
element: VideoRTC,
): MediaTechnology[] | undefined => {
const tech = [
...(!!element.pc ? ['webrtc'] : []),
...(!element.pc && element.mseCodecs ? ['mse', 'hls'] : []),
];
return tech.length ? tech : undefined;
};
+1 -1
View File
@@ -165,7 +165,7 @@ export class MenuController {
}
public getFreshButtonState(hass: HomeAssistant, button: MenuItem): StateParameters {
const stateParameters = { ...button };
const stateParameters = { ...button } as StateParameters;
return hass && button.type === 'custom:frigate-card-menu-state-icon'
? refreshDynamicStateParameters(hass, stateParameters)
: stateParameters;
+119
View File
@@ -0,0 +1,119 @@
import { HASSDomEvent } from '@dermotduffy/custom-card-helpers';
import { LitElement } from 'lit';
import { isEqual, orderBy } from 'lodash-es';
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request';
import {
ActionsConfig,
FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
StatusBarConfig,
StatusBarItem,
} from '../config/types';
import { getActionConfigGivenAction } from '../utils/action';
import { arrayify, setOrRemoveAttribute } from '../utils/basic';
import { Timer } from '../utils/timer';
export class StatusBarController {
protected _host: LitElement;
protected _config: StatusBarConfig | null = null;
protected _popupTimer = new Timer();
protected _items: StatusBarItem[] = [];
constructor(host: LitElement) {
this._host = host;
}
public getRenderItems(): StatusBarItem[] {
return this._items;
}
public setItems(items: StatusBarItem[]): void {
const exclusiveItems = items.filter((item) => !!item.exclusive);
const newItems = orderBy(
exclusiveItems.length ? exclusiveItems : items,
(item) => item.priority ?? FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
'desc',
);
const sufficientBefore = this._getSufficientValues(this._items);
const sufficientAfter = this._getSufficientValues(newItems);
this._items = newItems;
if (this._config?.style === 'popup' && !isEqual(sufficientBefore, sufficientAfter)) {
this._show();
this._popupTimer.start(this._config.popup_seconds, () => this._hide());
}
this._host.requestUpdate();
}
public setConfig(config: StatusBarConfig): void {
this._config = config;
this._host.style.setProperty(
'--frigate-card-status-bar-height',
`${config.height}px`,
);
this._host.setAttribute('data-style', config.style);
this._host.setAttribute('data-position', config.position);
if (this._config?.style !== 'popup') {
this._show();
}
this._host.requestUpdate();
}
public shouldRender(): boolean {
return this._items.some((item) => item.enabled !== false && item.sufficient);
}
public actionHandler(
ev: HASSDomEvent<{ action: string; config?: ActionsConfig }>,
config?: ActionsConfig,
): void {
// These interactions should only be handled by the status bar, as nothing
// upstream has the user-provided configuration.
ev.stopPropagation();
const interaction: string = ev.detail.action;
const action = getActionConfigGivenAction(interaction, config);
if (!action) {
return;
}
dispatchActionExecutionRequest(this._host, {
action: arrayify(action),
config: config,
});
}
protected _getSufficientValue(item: StatusBarItem): string | null {
/* istanbul ignore else: cannot happen -- @preserve */
if (item.type === 'custom:frigate-card-status-bar-icon') {
return item.icon;
} else if (item.type === 'custom:frigate-card-status-bar-string') {
return item.string;
} else if (item.type === 'custom:frigate-card-status-bar-image') {
return item.image;
} else {
return null;
}
}
protected _getSufficientValues(items: StatusBarItem[]): (string | null)[] {
return items
.filter((item) => item.enabled !== false && item.sufficient)
.map((item) => this._getSufficientValue(item));
}
protected _show(): void {
setOrRemoveAttribute(this._host, false, 'hide');
}
protected _hide(): void {
setOrRemoveAttribute(this._host, true, 'hide');
}
}
+107 -52
View File
@@ -20,6 +20,10 @@ import {
MenuSubmenu,
MenuSubmenuSelect,
PictureElements,
StatusBarIcon,
StatusBarImage,
StatusBarItem,
StatusBarString,
} from '../config/types.js';
import { localize } from '../localize/localize.js';
import elementsStyle from '../scss/elements.scss';
@@ -164,50 +168,66 @@ export class FrigateCardElements extends LitElement {
@property({ attribute: false })
public elements: PictureElements;
protected _boundMenuRemoveHandler = this._menuRemoveHandler.bind(this);
/**
* Handle a picture element to be removed from the menu.
* @param ev The event.
*/
protected _menuRemoveHandler(ev: Event): void {
// Re-dispatch event from this element (instead of the disconnected one, as
// there is no parent of the disconnected element).
dispatchFrigateCardEvent<MenuItem>(this, 'menu-remove', (ev as CustomEvent).detail);
protected _addHandler(
target: EventTarget,
eventName: string,
handler: (ev: Event) => void,
) {
// Ensure listener is only attached 1 time by removing it first.
target.removeEventListener(eventName, handler);
target.addEventListener(eventName, handler);
}
/**
* Handle a picture element to be added to the menu.
* @param ev The event.
*/
protected _menuAddHandler(ev: Event): void {
protected _menuRemoveHandler = (ev: Event): void => {
// Re-dispatch event from this element (instead of the disconnected one, as
// there is no parent of the disconnected element).
dispatchFrigateCardEvent<MenuItem>(this, 'menu:remove', (ev as CustomEvent).detail);
};
protected _statusBarRemoveHandler = (ev: Event): void => {
// Re-dispatch event from this element (instead of the disconnected one, as
// there is no parent of the disconnected element).
dispatchFrigateCardEvent<StatusBarItem>(
this,
'status-bar:remove',
(ev as CustomEvent).detail,
);
};
protected _menuAddHandler = (ev: Event): void => {
ev = ev as CustomEvent<MenuItem>;
const path = ev.composedPath();
if (!path.length) {
return;
}
this._addHandler(path[0], 'frigate-card:menu:remove', this._menuRemoveHandler);
};
// See 'A note on custom elements' above to explain what's going on here.
// Ensure listener is only attached 1 time by removing it first.
path[0].removeEventListener(
'frigate-card:menu-remove',
this._boundMenuRemoveHandler,
protected _statusBarAddHandler = (ev: Event): void => {
ev = ev as CustomEvent<MenuItem>;
const path = ev.composedPath();
if (!path.length) {
return;
}
this._addHandler(
path[0],
'frigate-card:status-bar:add',
this._statusBarRemoveHandler,
);
path[0].addEventListener('frigate-card:menu-remove', this._boundMenuRemoveHandler);
}
};
connectedCallback(): void {
super.connectedCallback();
// Catch icons being added to the menu (so their removal can be subsequently
// handled).
this.addEventListener('frigate-card:menu-add', this._menuAddHandler);
// Catch icons being added to the menu or status-bar (so their removal can
// be subsequently handled).
this.addEventListener('frigate-card:menu:add', this._menuAddHandler);
this.addEventListener('frigate-card:status-bar:add', this._statusBarAddHandler);
}
disconnectedCallback(): void {
this.removeEventListener('frigate-card:menu-add', this._menuAddHandler);
this.removeEventListener('frigate-card:menu:add', this._menuAddHandler);
this.addEventListener('frigate-card:status-bar:add', this._statusBarAddHandler);
super.disconnectedCallback();
}
@@ -286,59 +306,94 @@ export class FrigateCardElementsConditional extends LitElement {
}
// A base class for rendering menu icons / menu state icons.
export class FrigateCardElementsBaseMenuIcon<T> extends LitElement {
@state()
protected _config: T | null = null;
export class FrigateCardElementsBaseItem<ConfigType> extends LitElement {
protected _eventCategory: string;
/**
* Set the card config.
* @param config The configuration.
*/
public setConfig(config: T): void {
constructor(eventCategory: string) {
super();
this._eventCategory = eventCategory;
}
@state()
protected _config: ConfigType | null = null;
public setConfig(config: ConfigType): void {
this._config = config;
}
/**
* Connected callback.
*/
connectedCallback(): void {
super.connectedCallback();
if (this._config) {
dispatchFrigateCardEvent<T>(this, 'menu-add', this._config);
dispatchFrigateCardEvent<ConfigType>(
this,
`${this._eventCategory}:add`,
this._config,
);
}
}
/**
* Disconnected callback.
*/
disconnectedCallback(): void {
if (this._config) {
dispatchFrigateCardEvent<T>(this, 'menu-remove', this._config);
dispatchFrigateCardEvent<ConfigType>(
this,
`${this._eventCategory}:remove`,
this._config,
);
}
super.disconnectedCallback();
}
}
export class FrigateCardElementsBaseMenuItem<
ConfigType,
> extends FrigateCardElementsBaseItem<ConfigType> {
constructor() {
super('menu');
}
}
@customElement('frigate-card-menu-icon')
export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuIcon<MenuIcon> {}
export class FrigateCardElementsMenuIcon extends FrigateCardElementsBaseMenuItem<MenuIcon> {}
@customElement('frigate-card-menu-state-icon')
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuIcon<MenuStateIcon> {}
export class FrigateCardElementsMenuStateIcon extends FrigateCardElementsBaseMenuItem<MenuStateIcon> {}
@customElement('frigate-card-menu-submenu')
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuIcon<MenuSubmenu> {}
export class FrigateCardElementsMenuSubmenu extends FrigateCardElementsBaseMenuItem<MenuSubmenu> {}
@customElement('frigate-card-menu-submenu-select')
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuIcon<MenuSubmenuSelect> {}
export class FrigateCardElementsMenuSubmenuSelect extends FrigateCardElementsBaseMenuItem<MenuSubmenuSelect> {}
export class FrigateCardElementsBaseStatusBarItem<
ConfigType,
> extends FrigateCardElementsBaseItem<ConfigType> {
constructor() {
super('status-bar');
}
}
@customElement('frigate-card-status-bar-icon')
export class FrigateCardElementsStatusBarIcon extends FrigateCardElementsBaseStatusBarItem<StatusBarIcon> {}
@customElement('frigate-card-status-bar-image')
export class FrigateCardElementsStatusBarImage extends FrigateCardElementsBaseStatusBarItem<StatusBarImage> {}
@customElement('frigate-card-status-bar-string')
export class FrigateCardElementsStatusBarString extends FrigateCardElementsBaseStatusBarItem<StatusBarString> {}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-conditional': FrigateCardElementsConditional;
'frigate-card-elements': FrigateCardElements;
'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect;
'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu;
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
'frigate-card-elements-core': FrigateCardElementsCore;
'frigate-card-menu-icon': FrigateCardElementsMenuIcon;
'frigate-card-menu-state-icon': FrigateCardElementsMenuStateIcon;
'frigate-card-menu-submenu': FrigateCardElementsMenuSubmenu;
'frigate-card-menu-submenu-select': FrigateCardElementsMenuSubmenuSelect;
'frigate-card-status-bar-icon': FrigateCardElementsStatusBarIcon;
'frigate-card-status-bar-image': FrigateCardElementsStatusBarImage;
'frigate-card-status-bar-string': FrigateCardElementsStatusBarString;
}
}
+4
View File
@@ -10,6 +10,7 @@ import {
dispatchMediaPlayEvent,
dispatchMediaVolumeChangeEvent,
} from '../../../utils/media-info';
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
/**
* Video player for go2rtc streaming application.
@@ -325,6 +326,7 @@ export class VideoRTC extends HTMLElement {
supportsPause: true,
hasAudio: mayHaveAudio(this.video),
},
technology: getTechnologyForVideoRTC(this),
});
};
this.video.onvolumechange = () => dispatchMediaVolumeChangeEvent(this);
@@ -641,6 +643,7 @@ export class VideoRTC extends HTMLElement {
receivedFirstFrame = true;
dispatchMediaLoadedEvent(this, this.video, {
player: this.containingPlayer,
technology: ['mjpeg'],
});
}
};
@@ -668,6 +671,7 @@ export class VideoRTC extends HTMLElement {
dispatchMediaLoadedEvent(this, video2, {
player: this.containingPlayer,
technology: ['mp4'],
});
}
+1
View File
@@ -141,6 +141,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
capabilities: {
supportsPause: true,
},
technology: ['jsmpeg'],
});
}
}
+10 -5
View File
@@ -1,8 +1,9 @@
import { Task } from '@lit-labs/task';
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
import { Task } from '@lit-labs/task';
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { CameraEndpoints } from '../../camera-manager/types.js';
import { getTechnologyForVideoRTC } from '../../components-lib/live/utils/get-technology-for-video-rtc.js';
import { CameraConfig, CardWideConfig } from '../../config/types.js';
import { localize } from '../../localize/localize.js';
import liveWebRTCCardStyle from '../../scss/live-webrtc-card.scss';
@@ -22,6 +23,7 @@ import {
import { screenshotMedia } from '../../utils/screenshot.js';
import { renderTask } from '../../utils/task.js';
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
import { VideoRTC } from './go2rtc/video-rtc.js';
// Create a wrapper for AlexxIT's WebRTC card
// - https://github.com/AlexxIT/WebRTC
@@ -104,15 +106,16 @@ export class FrigateCardLiveWebRTCCard
this.requestUpdate();
}
protected _getVideoRTC(): VideoRTC | null {
return (this.renderRoot?.querySelector('#webrtc') ?? null) as VideoRTC | null;
}
/**
* Get the underlying video player.
* @returns The player or `null` if not found.
*/
protected _getPlayer(): HTMLVideoElement | null {
const root = this.renderRoot?.querySelector('#webrtc') as
| (HTMLElement & { video?: HTMLVideoElement })
| null;
return root?.video ?? null;
return this._getVideoRTC()?.video ?? null;
}
protected async _getWebRTCCardElement(): Promise<
@@ -192,6 +195,7 @@ export class FrigateCardLiveWebRTCCard
// Extract the video component after it has been rendered and generate the
// media load event.
this.updateComplete.then(() => {
const videoRTC = this._getVideoRTC();
const video = this._getPlayer();
if (video) {
setControlsOnVideo(video, this.controls);
@@ -205,6 +209,7 @@ export class FrigateCardLiveWebRTCCard
supportsPause: true,
hasAudio: mayHaveAudio(video),
},
...(videoRTC && { technology: getTechnologyForVideoRTC(videoRTC) }),
});
};
video.onplay = () => dispatchMediaPlayEvent(this);
-29
View File
@@ -62,11 +62,6 @@ import '../next-prev-control.js';
import '../ptz.js';
import { FrigateCardPTZ } from '../ptz.js';
import '../surround.js';
import '../title-control.js';
import {
FrigateCardTitleControl,
getDefaultTitleConfigForView,
} from '../title-control.js';
const FRIGATE_CARD_LIVE_PROVIDER = 'frigate-card-live-provider';
@@ -294,7 +289,6 @@ export class FrigateCardLiveCarousel extends LitElement {
// Index between camera name and slide number.
protected _cameraToSlide: Record<string, number> = {};
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
protected _refPTZControl: Ref<FrigateCardPTZ> = createRef();
@state()
@@ -540,18 +534,10 @@ export class FrigateCardLiveCarousel extends LitElement {
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(prevID, view))
: null;
const cameraID = this.viewFilterCameraID ?? view.camera;
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
this._getSubstreamCameraID(cameraID, view),
);
const cameraMetadataNext = nextID
? this.cameraManager.getCameraMetadata(this._getSubstreamCameraID(nextID, view))
: null;
const titleConfig = getDefaultTitleConfigForView(
view,
this.overriddenLiveConfig?.controls.title,
);
// Notes on the below:
// - guard() is used to avoid reseting the carousel unless the
// options/plugins actually change.
@@ -574,9 +560,6 @@ export class FrigateCardLiveCarousel extends LitElement {
transitionEffect=${this._getTransitionEffect()}
@frigate-card:carousel:select=${this._setViewHandler.bind(this)}
@frigate-card:media:loaded=${() => {
if (this._refTitleControl.value) {
this._refTitleControl.value.show();
}
this._mediaHasLoaded = true;
}}
@frigate-card:media:unloaded=${() => {
@@ -620,18 +603,6 @@ export class FrigateCardLiveCarousel extends LitElement {
.forceVisibility=${this._mediaHasLoaded && view.context?.ptzControls?.enabled}
>
</frigate-card-ptz>
${cameraMetadataCurrent && titleConfig
? html`<frigate-card-title-control
${ref(this._refTitleControl)}
.config=${titleConfig}
.text="${cameraMetadataCurrent
? `${localize('common.live')}: ${cameraMetadataCurrent.title}`
: ''}"
.logo="${cameraMetadataCurrent.engineLogo}"
.fitInto=${this as HTMLElement}
>
</frigate-card-title-control> `
: ``}
`;
}
+25
View File
@@ -0,0 +1,25 @@
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement } from 'lit/decorators.js';
import overlayStyle from '../scss/overlay.scss';
@customElement('frigate-card-overlay')
export class FrigateCardOverlay extends LitElement {
protected render(): TemplateResult | void {
return html`
<slot name="top"></slot>
<slot name="left"></slot>
<slot name="right"></slot>
<slot name="bottom"></slot>
`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(overlayStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-overlay': FrigateCardOverlay;
}
}
+91
View File
@@ -0,0 +1,91 @@
import { CSSResultGroup, LitElement, PropertyValues, TemplateResult, html, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { actionHandler } from '../action-handler-directive.js';
import { StatusBarController } from '../components-lib/status-bar-controller';
import { StatusBarConfig, StatusBarItem } from '../config/types';
import statusStyle from '../scss/status.scss';
import { frigateCardHasAction } from '../utils/action';
@customElement('frigate-card-status-bar')
export class FrigateCardStatusBar extends LitElement {
protected _controller = new StatusBarController(this);
@property({ attribute: false })
public items?: StatusBarItem[];
@property({ attribute: false })
public config?: StatusBarConfig;
protected willUpdate(changedProperties: PropertyValues): void {
// Always set config before items.
if (changedProperties.has('config') && this.config) {
this._controller.setConfig(this.config);
}
if (changedProperties.has('items')) {
this._controller.setItems(this.items ?? []);
}
}
protected render(): TemplateResult | void {
if (!this._controller.shouldRender()) {
return;
}
return html`
<div class="status">
${this._controller.getRenderItems().map((item): TemplateResult | void => {
if (item.enabled === false) {
return;
}
const classes = classMap({
item: true,
expand: !!item.expand,
action: !!Object.keys(item.actions ?? {}).length,
});
const handler = actionHandler({
hasHold: frigateCardHasAction(item.actions?.hold_action),
hasDoubleClick: frigateCardHasAction(item.actions?.double_tap_action),
});
if (item.type === 'custom:frigate-card-status-bar-string') {
return html`<div
.actionHandler=${handler}
class="${classes}"
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
>
${item.string}
</div>`;
} else if (item.type === 'custom:frigate-card-status-bar-icon') {
return html`<ha-icon
.actionHandler=${handler}
class="${classes}"
icon="${item.icon}"
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
></ha-icon>`;
} else if (item.type === 'custom:frigate-card-status-bar-image') {
return html`<img
.actionHandler=${handler}
class="${classes}"
src="${item.image}"
@action=${(ev) => this._controller.actionHandler(ev, item.actions)}
/>`;
}
})}
</div>
`;
}
static get styles(): CSSResultGroup {
return unsafeCSS(statusStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-status-bar': FrigateCardStatusBar;
}
}
+5 -2
View File
@@ -81,6 +81,7 @@ export class FrigateCardSubmenu extends LitElement {
if (!this.submenu) {
return html``;
}
const items = this.submenu.items as MenuSubmenuItem[];
return html`
<ha-button-menu
corner=${'BOTTOM_LEFT'}
@@ -109,7 +110,7 @@ export class FrigateCardSubmenu extends LitElement {
>
<ha-icon icon="${this.submenu.icon}"></ha-icon>
</ha-icon-button>
${this.submenu.items.map(this._renderItem.bind(this))}
${items.map(this._renderItem.bind(this))}
</ha-button-menu>
`;
}
@@ -217,9 +218,11 @@ export class FrigateCardSubmenuSelect extends LitElement {
// the items correctly (below).
delete submenu['options'];
const items = submenu.items as MenuSubmenuItem[];
for (const option of options) {
const title = this._optionTitles?.[option] ?? option;
submenu.items.push({
items.push({
state_color: true,
selected: stateObj.state === option,
enabled: true,
-4
View File
@@ -51,10 +51,6 @@ export class FrigateCardSurroundBasic extends LitElement {
}
}
/**
* Master render method.
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
return html` <slot name="above"></slot>
<slot></slot>
-98
View File
@@ -1,98 +0,0 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import { TitleControlConfig } from '../config/types';
import titleStyle from '../scss/title-control.scss';
import { View } from '../view/view.js';
type PaperToast = HTMLElement & {
opened: boolean;
};
export const getDefaultTitleConfigForView = (
view?: Readonly<View> | null,
baseConfig?: TitleControlConfig,
): TitleControlConfig | null => {
if (!baseConfig && view?.isGrid()) {
return { mode: 'none', duration_seconds: 2 };
}
return {
mode: 'popup-bottom-right',
duration_seconds: 2,
...baseConfig,
};
};
@customElement('frigate-card-title-control')
export class FrigateCardTitleControl extends LitElement {
@property({ attribute: false })
public config?: TitleControlConfig;
@property({ attribute: false })
public text?: string;
@property({ attribute: false })
public fitInto?: HTMLElement;
@property({ attribute: false })
public logo?: string;
protected _toastRef: Ref<PaperToast> = createRef();
protected render(): TemplateResult {
if (
!this.text ||
!this.config ||
!this.config.mode ||
this.config.duration_seconds === undefined ||
this.config.mode === 'none' ||
!this.fitInto
) {
return html``;
}
const verticalAlign = this.config.mode.match(/-top-/) ? 'top' : 'bottom';
const horizontalAlign = this.config.mode.match(/-left$/) ? 'left' : 'right';
return html` <paper-toast
${ref(this._toastRef)}
class="capsule"
.duration=${this.config.duration_seconds * 1000}
.verticalAlign=${verticalAlign}
.horizontalAlign=${horizontalAlign}
.text="${this.text}"
.fitInto=${this.fitInto}
>
${this.logo ? html`<img src=${this.logo} />` : ''}
</paper-toast>`;
}
public isVisible(): boolean {
return this._toastRef.value?.opened ?? false;
}
public hide(): void {
if (this._toastRef.value) {
// Set it to false first, to ensure the timer resets.
this._toastRef.value.opened = false;
}
}
public show(): void {
if (this._toastRef.value) {
// Set it to false first, to ensure the timer resets.
this._toastRef.value.opened = false;
this._toastRef.value.opened = true;
}
}
static get styles(): CSSResultGroup {
return unsafeCSS(titleStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'frigate-card-title-control': FrigateCardTitleControl;
}
}
+2 -28
View File
@@ -73,11 +73,6 @@ import type { EmblaCarouselPlugins } from './carousel.js';
import './next-prev-control.js';
import './ptz';
import './surround.js';
import './title-control.js';
import {
FrigateCardTitleControl,
getDefaultTitleConfigForView,
} from './title-control.js';
export interface MediaViewerViewContext {
seek?: Date;
@@ -194,7 +189,6 @@ export class FrigateCardViewerCarousel extends LitElement {
protected _selected = 0;
protected _media: ViewMedia[] | null = null;
protected _refTitleControl: Ref<FrigateCardTitleControl> = createRef();
protected _player: FrigateCardMediaPlayer | null = null;
/**
@@ -407,15 +401,7 @@ export class FrigateCardViewerCarousel extends LitElement {
}
};
const cameraMetadata = this.cameraManager.getCameraMetadata(
selectedMedia.getCameraID(),
);
const view = this.viewManagerEpoch?.manager.getView();
const titleConfig = getDefaultTitleConfigForView(
view,
this.viewerConfig?.controls.title,
);
return html`
<frigate-card-carousel
@@ -427,9 +413,6 @@ export class FrigateCardViewerCarousel extends LitElement {
this._setViewSelectedIndex(ev.detail.index);
}}
@frigate-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
if (this._refTitleControl.value) {
this._refTitleControl.value.show();
}
this._player = ev.detail.player ?? null;
this._seekHandler();
}}
@@ -480,16 +463,6 @@ export class FrigateCardViewerCarousel extends LitElement {
<ha-icon title="${localize('media_viewer.unseekable')}" icon="mdi:clock-remove">
</ha-icon>
</div>
${cameraMetadata && titleConfig
? html`<frigate-card-title-control
${ref(this._refTitleControl)}
.config=${titleConfig}
.text="${selectedMedia.getTitle() ?? undefined}"
.logo="${cameraMetadata?.engineLogo}"
.fitInto=${this as HTMLElement}
>
</frigate-card-title-control> `
: ``}
`;
}
@@ -919,6 +892,7 @@ export class FrigateCardViewerProvider
supportsPause: true,
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
},
technology: ['hls'],
});
}}
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
@@ -942,7 +916,7 @@ export class FrigateCardViewerProvider
}
}}
@load=${(ev: Event) => {
dispatchMediaLoadedEvent(this, ev, { player: this });
dispatchMediaLoadedEvent(this, ev, { player: this, technology: ['jpg'] });
}}
/>`}
`);
+34 -7
View File
@@ -3,11 +3,6 @@ import get from 'lodash-es/get';
import isEqual from 'lodash-es/isEqual';
import set from 'lodash-es/set';
import unset from 'lodash-es/unset';
import {
FrigateCardCondition,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
} from './types';
import {
CONF_AUTOMATIONS,
CONF_CAMERAS,
@@ -24,18 +19,24 @@ import {
CONF_MENU_BUTTONS_CAMERA_UI,
CONF_OVERRIDES,
CONF_PROFILES,
CONF_STATUS_BAR,
CONF_TIMELINE_EVENTS_MEDIA_TYPE,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS_TRIGGER,
CONF_VIEW_TRIGGERS_ACTIONS_UNTRIGGER,
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
} from '../const';
import { arrayify } from '../utils/basic';
import {
FrigateCardCondition,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
} from './types';
// *************************************************************************
// General Config Management Functions
@@ -660,6 +661,27 @@ const ptzControlSettingsTransform = (data: unknown): unknown => {
}, {});
};
const titleControlTransform = (data: unknown): unknown => {
if (typeof data !== 'object' || !data || typeof data['mode'] !== 'string') {
return null;
}
if (data['mode'] === 'none') {
return {
style: 'none',
};
}
if (data['mode'].includes('bottom')) {
return {
position: 'bottom',
};
} else if (data['mode'].includes('top')) {
return {
position: 'top',
};
}
return null;
};
const UPGRADES = [
// v4.0.0 -> v4.1.0
upgradeArrayOfObjects(
@@ -822,4 +844,9 @@ const UPGRADES = [
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
),
upgradeMoveToWithOverrides('view.update_entities', CONF_VIEW_DEFAULT_RESET_ENTITIES),
upgradeMoveTo('live.controls.title', CONF_STATUS_BAR, {
transform: titleControlTransform,
}),
deleteWithOverrides('live.controls.title'),
deleteWithOverrides('media_viewer.controls.title'),
];
+3 -4
View File
@@ -9,7 +9,6 @@ import {
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_LIVE_CONTROLS_TITLE_MODE,
CONF_LIVE_DRAGGABLE,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
@@ -28,7 +27,6 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
@@ -40,6 +38,7 @@ import {
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_STATUS_BAR_STYLE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL,
@@ -83,8 +82,8 @@ export const LOW_PERFORMANCE_PROFILE = {
// Media player next/previous are chevrons.
[CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE]: 'chevrons' as const,
[CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE]: 'none' as const,
[CONF_LIVE_CONTROLS_TITLE_MODE]: 'none' as const,
// Disable the status bar.
[CONF_STATUS_BAR_STYLE]: 'none' as const,
// Move the menu to outside to remove the need to interact with it with open.
[CONF_MENU_STYLE]: 'outside',
+138 -26
View File
@@ -21,12 +21,15 @@ import { PTZ_ACTIONS } from './ptz';
// Common Configuration Constants
// *************************************************************************
// The min allowed size of buttons.
export const BUTTON_SIZE_MIN = 20;
export const STATUS_BAR_HEIGHT_MIN = BUTTON_SIZE_MIN;
const FRIGATE_MENU_PRIORITY_DEFAULT = 50;
export const FRIGATE_MENU_PRIORITY_MAX = 100;
export const FRIGATE_STATUS_BAR_PRIORITY_DEFAULT = FRIGATE_MENU_PRIORITY_DEFAULT;
export const FRIGATE_STATUS_BAR_PRIORITY_MAX = FRIGATE_MENU_PRIORITY_MAX;
export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
'diagnostics',
'live',
@@ -343,6 +346,19 @@ const sleepActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
});
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
const statusBarActionConfigSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('status_bar'),
status_bar_action: z.enum(['add', 'remove', 'reset']),
// This needs to be lazily evaluated since statusBarItemSchema may itself
// contain actions.
items: z
.lazy(() => statusBarItemSchema)
.array()
.optional(),
});
export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchema>;
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
@@ -366,6 +382,7 @@ export const frigateCardCustomActionSchema = z.union([
viewActionConfigSchema,
viewDisplayModeActionConfigSchema,
sleepActionConfigSchema,
statusBarActionConfigSchema,
]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -581,6 +598,52 @@ const menuSubmenuSelectSchema = menuBaseSchema.merge(stateIconSchema).extend({
export type MenuSubmenuSelect = z.infer<typeof menuSubmenuSelectSchema>;
export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelect;
// *************************************************************************
// Custom Element Configuration: Status bar
// *************************************************************************
const statusBarItemBaseSchema = z.object({
enabled: z.boolean().default(true).optional(),
priority: z
.number()
.min(0)
.max(FRIGATE_STATUS_BAR_PRIORITY_MAX)
.default(FRIGATE_STATUS_BAR_PRIORITY_DEFAULT)
.optional(),
});
const statusBarItemElementsBaseSchema = statusBarItemBaseSchema.extend({
sufficient: z.boolean().default(false).optional(),
exclusive: z.boolean().default(false).optional(),
expand: z.boolean().default(false).optional(),
actions: actionsBaseSchema.optional(),
});
const statusBarIconItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-icon'),
icon: z.string(),
});
export type StatusBarIcon = z.infer<typeof statusBarIconItemSchema>;
const statusBarImageItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-image'),
image: z.string(),
});
export type StatusBarImage = z.infer<typeof statusBarImageItemSchema>;
const statusBarStringItemSchema = statusBarItemElementsBaseSchema.extend({
type: z.literal('custom:frigate-card-status-bar-string'),
string: z.string(),
});
export type StatusBarString = z.infer<typeof statusBarStringItemSchema>;
const statusBarItemSchema = z.union([
statusBarIconItemSchema,
statusBarImageItemSchema,
statusBarStringItemSchema,
]);
export type StatusBarItem = z.infer<typeof statusBarItemSchema>;
// *************************************************************************
// Custom Element Configuration: Conditions
// *************************************************************************
@@ -680,6 +743,9 @@ const pictureElementSchema = z.union([
stateBadgeIconSchema,
stateIconSchema,
stateLabelSchema,
statusBarIconItemSchema,
statusBarImageItemSchema,
statusBarStringItemSchema,
]);
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
@@ -1009,24 +1075,6 @@ const nextPreviousControlConfigSchema = z.object({
});
export type NextPreviousControlConfig = z.infer<typeof nextPreviousControlConfigSchema>;
// *************************************************************************
// Title Control Configuration
// *************************************************************************
const titleControlConfigSchema = z.object({
mode: z
.enum([
'none',
'popup-top-right',
'popup-top-left',
'popup-bottom-right',
'popup-bottom-left',
])
.optional(),
duration_seconds: z.number().min(0).max(60).optional(),
});
export type TitleControlConfig = z.infer<typeof titleControlConfigSchema>;
// *************************************************************************
// Carousel Transition Configuration
// *************************************************************************
@@ -1188,7 +1236,6 @@ const liveConfigSchema = z
liveConfigDefault.controls.thumbnails,
),
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
title: titleControlConfigSchema.optional(),
})
.default(liveConfigDefault.controls),
display: viewDisplaySchema,
@@ -1584,6 +1631,65 @@ export const menuConfigSchema = z
.default(menuConfigDefault);
export type MenuConfig = z.infer<typeof menuConfigSchema>;
// *************************************************************************
// Status Bar Configuration
// *************************************************************************
const STATUS_BAR_STYLES = [
'none',
'overlay',
'hover',
'hover-card',
'outside',
'popup',
] as const;
const STATUS_BAR_POSITIONS = ['top', 'bottom'] as const;
const statusBarItemDefault = {
priority: FRIGATE_STATUS_BAR_PRIORITY_DEFAULT,
enabled: true,
};
const statusBarConfigDefault = {
height: 46,
items: {
engine: statusBarItemDefault,
resolution: statusBarItemDefault,
technology: statusBarItemDefault,
title: statusBarItemDefault,
},
position: 'bottom' as const,
style: 'popup' as const,
popup_seconds: 3,
};
export const statusBarConfigSchema = z
.object({
position: z.enum(STATUS_BAR_POSITIONS).default(statusBarConfigDefault.position),
style: z.enum(STATUS_BAR_STYLES).default(statusBarConfigDefault.style),
popup_seconds: z
.number()
.min(0)
.max(60)
.default(statusBarConfigDefault.popup_seconds),
height: z.number().min(STATUS_BAR_HEIGHT_MIN).default(statusBarConfigDefault.height),
items: z
.object({
engine: statusBarItemBaseSchema.default(statusBarConfigDefault.items.engine),
technology: statusBarItemBaseSchema.default(
statusBarConfigDefault.items.technology,
),
resolution: statusBarItemBaseSchema.default(
statusBarConfigDefault.items.resolution,
),
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
})
.default(statusBarConfigDefault.items),
})
.default(statusBarConfigDefault);
export type StatusBarConfig = z.infer<typeof statusBarConfigSchema>;
// *************************************************************************
// Event Viewer Configuration
// *************************************************************************
@@ -1672,7 +1778,6 @@ const viewerConfigSchema = z
timeline: miniTimelineConfigSchema.default(
viewerConfigDefault.controls.timeline,
),
title: titleControlConfigSchema.optional(),
})
.default(viewerConfigDefault.controls),
})
@@ -1767,11 +1872,16 @@ export type Overrides = z.infer<typeof overridesSchema>;
const automationActionSchema = actionSchema.array();
export type AutomationActions = z.infer<typeof automationActionSchema>;
const automationSchema = z.object({
conditions: frigateCardConditionSchema.array(),
actions: automationActionSchema.optional(),
actions_not: automationActionSchema.optional(),
});
const automationSchema = z
.object({
conditions: frigateCardConditionSchema.array(),
actions: automationActionSchema.optional(),
actions_not: automationActionSchema.optional(),
})
.refine(
(data) => data.actions?.length || data.actions_not?.length,
'Automations must include at least one action',
);
export type Automation = z.infer<typeof automationSchema>;
const automationsSchema = automationSchema.array();
@@ -1857,6 +1967,7 @@ export const frigateCardConfigSchema = z.object({
view: viewConfigSchema,
menu: menuConfigSchema,
status_bar: statusBarConfigSchema,
live: liveConfigSchema,
media_gallery: galleryConfigSchema,
media_viewer: viewerConfigSchema,
@@ -1892,6 +2003,7 @@ export const frigateCardConfigDefaults = {
cameras: cameraConfigDefault,
view: viewConfigDefault,
menu: menuConfigDefault,
status_bar: statusBarConfigDefault,
live: liveConfigDefault,
media_gallery: galleryConfigDefault,
media_viewer: viewerConfigDefault,
+8 -8
View File
@@ -216,11 +216,6 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS =
`${CONF_MEDIA_VIEWER}.controls.timeline.window_seconds` as const;
export const CONF_MEDIA_VIEWER_ZOOMABLE = `${CONF_MEDIA_VIEWER}.zoomable` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE =
`${CONF_MEDIA_VIEWER}.controls.title.mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_MEDIA_VIEWER}.controls.title.duration_seconds` as const;
const CONF_LIVE = 'live' as const;
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
export const CONF_LIVE_AUTO_PAUSE = `${CONF_LIVE}.auto_pause` as const;
@@ -272,9 +267,6 @@ export const CONF_LIVE_CONTROLS_TIMELINE_STYLE =
`${CONF_LIVE}.controls.timeline.style` as const;
export const CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS =
`${CONF_LIVE}.controls.timeline.window_seconds` as const;
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` as const;
export const CONF_LIVE_DISPLAY_MODE = `${CONF_LIVE}.display.mode` as const;
export const CONF_LIVE_DISPLAY_GRID_COLUMNS =
`${CONF_LIVE}.display.grid_columns` as const;
@@ -336,6 +328,14 @@ export const CONF_MENU_BUTTONS_MEDIA_PLAYER =
`${CONF_MENU}.buttons.media_player` as const;
export const CONF_MENU_BUTTONS_TIMELINE = `${CONF_MENU}.buttons.timeline` as const;
export const CONF_STATUS_BAR = 'status_bar' as const;
export const CONF_STATUS_BAR_POSITION = `${CONF_STATUS_BAR}.position` as const;
export const CONF_STATUS_BAR_STYLE = `${CONF_STATUS_BAR}.style` as const;
export const CONF_STATUS_BAR_POPUP_SECONDS = `${CONF_STATUS_BAR}.popup_seconds` as const;
export const CONF_STATUS_BAR_HEIGHT = `${CONF_STATUS_BAR}.height` as const;
export const CONF_STATUS_BAR_ITEMS = `${CONF_STATUS_BAR}.items` as const;
const CONF_DIMENSIONS = 'dimensions' as const;
export const CONF_DIMENSIONS_ASPECT_RATIO = `${CONF_DIMENSIONS}.aspect_ratio` as const;
export const CONF_DIMENSIONS_ASPECT_RATIO_MODE =
+124 -131
View File
@@ -11,6 +11,8 @@ import {
MOTIONEYE_ICON_SVG_PATH,
MOTIONEYE_ICON_SVG_VIEWBOX,
} from './camera-manager/motioneye/icon.js';
import './components/key-assigner.js';
import { KeyboardShortcut } from './config/keyboard-shortcuts.js';
import {
copyConfig,
deleteConfigValue,
@@ -23,12 +25,14 @@ import {
import { setProfiles } from './config/profiles/index.js';
import {
BUTTON_SIZE_MIN,
FRIGATE_MENU_PRIORITY_MAX,
FRIGATE_STATUS_BAR_PRIORITY_MAX,
FrigateCardConfig,
frigateCardConfigDefaults,
FRIGATE_MENU_PRIORITY_MAX,
profilesSchema,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
STATUS_BAR_HEIGHT_MIN,
THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN,
} from './config/types.js';
@@ -112,8 +116,6 @@ import {
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_LIVE_CONTROLS_TIMELINE_STYLE,
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
CONF_LIVE_CONTROLS_TITLE_MODE,
CONF_LIVE_DISPLAY_GRID_COLUMNS,
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
CONF_LIVE_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
@@ -154,8 +156,6 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_WIDTH_FACTOR,
@@ -166,8 +166,8 @@ import {
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MEDIA_VIEWER_ZOOMABLE,
CONF_MENU_ALIGNMENT,
CONF_MENU_BUTTONS,
CONF_MENU_BUTTON_SIZE,
CONF_MENU_BUTTONS,
CONF_MENU_POSITION,
CONF_MENU_STYLE,
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
@@ -176,6 +176,11 @@ import {
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_PROFILES,
CONF_STATUS_BAR_HEIGHT,
CONF_STATUS_BAR_ITEMS,
CONF_STATUS_BAR_POPUP_SECONDS,
CONF_STATUS_BAR_POSITION,
CONF_STATUS_BAR_STYLE,
CONF_TIMELINE_CLUSTERING_THRESHOLD,
CONF_TIMELINE_CONTROLS_THUMBNAILS_MODE,
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_DETAILS,
@@ -190,6 +195,12 @@ import {
CONF_VIEW_CAMERA_SELECT,
CONF_VIEW_DARK_MODE,
CONF_VIEW_DEFAULT,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
CONF_VIEW_DEFAULT_RESET,
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_INTERACTION_SECONDS,
CONF_VIEW_KEYBOARD_SHORTCUTS,
CONF_VIEW_KEYBOARD_SHORTCUTS_ENABLED,
@@ -200,7 +211,6 @@ import {
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_UP,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_IN,
CONF_VIEW_KEYBOARD_SHORTCUTS_PTZ_ZOOM_OUT,
CONF_VIEW_DEFAULT_RESET_AFTER_INTERACTION,
CONF_VIEW_TRIGGERS,
CONF_VIEW_TRIGGERS_ACTIONS,
CONF_VIEW_TRIGGERS_ACTIONS_INTERACTION_MODE,
@@ -209,12 +219,7 @@ import {
CONF_VIEW_TRIGGERS_FILTER_SELECTED_CAMERA,
CONF_VIEW_TRIGGERS_SHOW_TRIGGER_STATUS,
CONF_VIEW_TRIGGERS_UNTRIGGER_SECONDS,
CONF_VIEW_DEFAULT_CYCLE_CAMERA,
MEDIA_CHUNK_SIZE_MAX,
CONF_VIEW_DEFAULT_RESET,
CONF_VIEW_DEFAULT_RESET_EVERY_SECONDS,
CONF_VIEW_DEFAULT_RESET_INTERACTION_MODE,
CONF_VIEW_DEFAULT_RESET_ENTITIES,
} from './const.js';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -225,10 +230,7 @@ import {
getEntityTitle,
sideLoadHomeAssistantElements,
} from './utils/ha';
import './components/key-assigner.js';
import { KeyboardShortcut } from './config/keyboard-shortcuts.js';
const MENU_BUTTONS = 'buttons';
const MENU_CAMERAS = 'cameras';
const MENU_CAMERAS_CAPABILITIES = 'cameras.capabilities';
const MENU_CAMERAS_CAST = 'cameras.cast';
@@ -248,7 +250,6 @@ const MENU_LIVE_CONTROLS_NEXT_PREVIOUS = 'live.controls.next_previous';
const MENU_LIVE_CONTROLS_PTZ = 'live.controls.ptz';
const MENU_LIVE_CONTROLS_THUMBNAILS = 'live.controls.thumbnails';
const MENU_LIVE_CONTROLS_TIMELINE = 'live.controls.timeline';
const MENU_LIVE_CONTROLS_TITLE = 'live.controls.title';
const MENU_LIVE_DISPLAY = 'live.display';
const MENU_LIVE_MICROPHONE = 'live.microphone';
const MENU_MEDIA_GALLERY_CONTROLS_FILTER = 'media_gallery.controls.filter';
@@ -257,14 +258,15 @@ const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
const MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS = 'media_viewer.controls.next_previous';
const MENU_MEDIA_VIEWER_CONTROLS_THUMBNAILS = 'media_viewer.controls.thumbnails';
const MENU_MEDIA_VIEWER_CONTROLS_TIMELINE = 'media_viewer.controls.timeline';
const MENU_MEDIA_VIEWER_CONTROLS_TITLE = 'media_viewer.controls.title';
const MENU_MEDIA_VIEWER_DISPLAY = 'media_viewer.display';
const MENU_MENU_BUTTONS = 'menu.buttons';
const MENU_OPTIONS = 'options';
const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_STATUS_BAR_ITEMS = 'status_bar.items';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
const MENU_VIEW_DEFAULT_RESET = 'view.default_reset';
const MENU_VIEW_KEYBOARD_SHORTCUTS = 'view.keyboard_shortcuts';
const MENU_VIEW_TRIGGERS = 'view.triggers';
const MENU_VIEW_TRIGGERS_ACTIONS = 'view.triggers.actions';
@@ -303,6 +305,11 @@ const options: EditorOptions = {
name: localize('editor.menu'),
secondary: localize('editor.menu_secondary'),
},
status_bar: {
icon: 'sign-text',
name: localize('editor.status_bar'),
secondary: localize('editor.status_bar_secondary'),
},
live: {
icon: 'cctv',
name: localize('editor.live'),
@@ -503,27 +510,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
},
];
protected _titleModes: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'none', label: localize('config.common.controls.title.modes.none') },
{
value: 'popup-top-left',
label: localize('config.common.controls.title.modes.popup-top-left'),
},
{
value: 'popup-top-right',
label: localize('config.common.controls.title.modes.popup-top-right'),
},
{
value: 'popup-bottom-left',
label: localize('config.common.controls.title.modes.popup-bottom-left'),
},
{
value: 'popup-bottom-right',
label: localize('config.common.controls.title.modes.popup-bottom-right'),
},
];
protected _transitionEffects: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'none', label: localize('config.media_viewer.transition_effects.none') },
@@ -831,6 +817,22 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
},
];
protected _statusBarStyles: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'hover', label: localize('config.status_bar.styles.hover') },
{ value: 'hover-card', label: localize('config.status_bar.styles.hover-card') },
{ value: 'none', label: localize('config.status_bar.styles.none') },
{ value: 'outside', label: localize('config.status_bar.styles.outside') },
{ value: 'overlay', label: localize('config.status_bar.styles.overlay') },
{ value: 'popup', label: localize('config.status_bar.styles.popup') },
];
protected _statusBarPositions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'top', label: localize('config.status_bar.positions.top') },
{ value: 'bottom', label: localize('config.status_bar.positions.bottom') },
];
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
@@ -1230,11 +1232,29 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
);
}
/**
* Render an editor menu for the card menu buttons.
* @param button The name of the button.
* @returns A rendered template.
*/
protected _renderStatusBarItem(item: string): TemplateResult {
return html` ${this._putInSubmenu(
MENU_STATUS_BAR_ITEMS,
item,
`config.status_bar.items.${item}`,
{ name: 'mdi:feature-search' },
html`
${this._renderSwitch(
`${CONF_STATUS_BAR_ITEMS}.${item}.enabled`,
this._defaults.status_bar.items[item]?.enabled ?? true,
{
label: localize('config.status_bar.items.enabled'),
},
)}
${this._renderNumberInput(`${CONF_STATUS_BAR_ITEMS}.${item}.priority`, {
max: FRIGATE_STATUS_BAR_PRIORITY_MAX,
default: this._defaults.status_bar.items[item]?.priority,
label: localize('config.status_bar.items.priority'),
})}
`,
)}`;
}
protected _renderMenuButton(
button: string,
additionalOptions?: TemplateResult,
@@ -1244,56 +1264,38 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'matching', label: localize('config.menu.buttons.alignments.matching') },
{ value: 'opposing', label: localize('config.menu.buttons.alignments.opposing') },
];
const submenuClasses = {
submenu: true,
selected: this._expandedMenus[MENU_BUTTONS] === button,
};
return html`
<div class="${classMap(submenuClasses)}">
<div
class="submenu-header"
@click=${this._toggleMenu}
.domain=${MENU_BUTTONS}
.key=${button}
>
<ha-icon .icon=${'mdi:gesture-tap-button'}></ha-icon>
<span
>${localize('editor.button') +
': ' +
localize(`config.${CONF_MENU_BUTTONS}.${button}`)}</span
>
</div>
${this._expandedMenus[MENU_BUTTONS] === button
? html` <div class="values">
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.enabled`,
this._defaults.menu.buttons[button]?.enabled ?? true,
{
label: localize('config.menu.buttons.enabled'),
},
)}
${this._renderOptionSelector(
`${CONF_MENU_BUTTONS}.${button}.alignment`,
menuButtonAlignments,
{
label: localize('config.menu.buttons.alignment'),
},
)}
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
max: FRIGATE_MENU_PRIORITY_MAX,
default: this._defaults.menu.buttons[button]?.priority,
label: localize('config.menu.buttons.priority'),
})}
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
label: localize('config.menu.buttons.icon'),
})}
${additionalOptions}
</div>`
: ''}
</div>
`;
return html` ${this._putInSubmenu(
MENU_MENU_BUTTONS,
button,
`config.menu.buttons.${button}`,
{ name: 'mdi:gesture-tap-button' },
html`
${this._renderSwitch(
`${CONF_MENU_BUTTONS}.${button}.enabled`,
this._defaults.menu.buttons[button]?.enabled ?? true,
{
label: localize('config.menu.buttons.enabled'),
},
)}
${this._renderOptionSelector(
`${CONF_MENU_BUTTONS}.${button}.alignment`,
menuButtonAlignments,
{
label: localize('config.menu.buttons.alignment'),
},
)}
${this._renderNumberInput(`${CONF_MENU_BUTTONS}.${button}.priority`, {
max: FRIGATE_MENU_PRIORITY_MAX,
default: this._defaults.menu.buttons[button]?.priority,
label: localize('config.menu.buttons.priority'),
})}
${this._renderIconSelector(`${CONF_MENU_BUTTONS}.${button}.icon`, {
label: localize('config.menu.buttons.icon'),
})}
${additionalOptions}
`,
)}`;
}
/**
@@ -1730,34 +1732,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
);
}
/**
* Render the titles controls.
* @param domain The submenu domain.
* @param configPathMode Title mode config path.
* @param configPathDurationSeconds Title duration seconds config path.
* @returns A rendered template.
*/
protected _renderTitleControls(
menuDomain: string,
configPathMode: string,
configPathDurationSeconds: string,
): TemplateResult | void {
return this._putInSubmenu(
menuDomain,
true,
'config.common.controls.title.editor_label',
{ name: 'mdi:subtitles' },
html` ${this._renderOptionSelector(configPathMode, this._titleModes, {
label: localize('config.common.controls.title.mode'),
})}
${this._renderNumberInput(configPathDurationSeconds, {
min: 0,
max: 60,
label: localize('config.common.controls.title.duration_seconds'),
})}`,
);
}
/**
* Render a camera section.
* @param cameras The full array of cameras.
@@ -2427,6 +2401,35 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>
`
: ''}
${this._renderOptionSetHeader('status_bar')}
${this._expandedMenus[MENU_OPTIONS] === 'status_bar'
? html`
<div class="values">
${this._renderOptionSelector(
CONF_STATUS_BAR_STYLE,
this._statusBarStyles,
)}
${this._renderOptionSelector(
CONF_STATUS_BAR_POSITION,
this._statusBarPositions,
)}
${this._renderNumberInput(CONF_STATUS_BAR_HEIGHT, {
min: STATUS_BAR_HEIGHT_MIN,
label: localize('config.status_bar.height'),
})}
${this._renderNumberInput(CONF_STATUS_BAR_POPUP_SECONDS, {
min: 0,
max: 60,
default: this._defaults.status_bar.popup_seconds,
label: localize('config.status_bar.popup_seconds'),
})}
${this._renderStatusBarItem('title') /* */}
${this._renderStatusBarItem('resolution') /* */}
${this._renderStatusBarItem('technology') /* */}
${this._renderStatusBarItem('engine') /* */}
</div>
`
: ''}
${this._renderOptionSetHeader('live')}
${this._expandedMenus[MENU_OPTIONS] === 'live'
? html`
@@ -2521,11 +2524,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathMode: CONF_LIVE_CONTROLS_THUMBNAILS_MODE,
},
)}
${this._renderTitleControls(
MENU_LIVE_CONTROLS_TITLE,
CONF_LIVE_CONTROLS_TITLE_MODE,
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
)}
${this._renderMiniTimeline(
MENU_LIVE_CONTROLS_TIMELINE,
CONF_LIVE_CONTROLS_TIMELINE_MODE,
@@ -2708,11 +2706,6 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
configPathMode: CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
},
)}
${this._renderTitleControls(
MENU_MEDIA_VIEWER_CONTROLS_TITLE,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
)}
${this._renderMiniTimeline(
MENU_MEDIA_VIEWER_CONTROLS_TIMELINE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE,
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "Cerca d'escombratge només a la càmera seleccionada",
"seek-in-media": "Cerca d'escombratge només dins de l'element multimèdia seleccionat"
}
},
"title": {
"duration_seconds": "Segons per mostrar el títol emergent (0=per sempre)",
"editor_label": "Controls del títol emergent",
"mode": "Mode de visualització del títol emergent",
"modes": {
"none": "No es mostra el títol",
"popup-bottom-left": "Popup a la part inferior esquerra",
"popup-bottom-right": "Popup a la part inferior dreta",
"popup-top-left": "Popup a la part superior esquerra",
"popup-top-right": "Popup a la part superior dreta"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "",
"scrubbing": ""
},
"status_bar": {
"height": "",
"items": {
"enabled": "",
"engine": "",
"live_provider": "",
"priority": "",
"resolution": "",
"technology": "",
"title": ""
},
"popup_seconds": "",
"position": "",
"positions": {
"bottom": "",
"top": ""
},
"style": "",
"styles": {
"hover": "",
"hover-card": "",
"none": "",
"outside": "",
"overlay": "",
"popup": ""
}
},
"view": {
"camera_select": "Visualitza les càmeres seleccionades recentment",
"dark_mode": "Mode fosc",
@@ -533,6 +548,8 @@
"performance_secondary": "Opcions de rendiment de la targeta",
"profiles": "",
"profiles_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Cronologia",
"timeline_secondary": "Opcions de la cronologia d'esdeveniments",
"upgrade": "Upgrade",
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "Pan seeks within selected camera only",
"seek-in-media": "Pan seeks within selected media item only"
}
},
"title": {
"duration_seconds": "Seconds to display popup title (0=forever)",
"editor_label": "Popup Title Controls",
"mode": "Popup title display mode",
"modes": {
"none": "No title display",
"popup-bottom-left": "Popup on the bottom left",
"popup-bottom-right": "Popup on the bottom right",
"popup-top-left": "Popup on the top left",
"popup-top-right": "Popup on the top right"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "Low performance",
"scrubbing": "Video scrubbing"
},
"status_bar": {
"height": "Status bar height in pixels",
"items": {
"enabled": "Item enabled",
"engine": "Camera engine",
"live_provider": "Live provider",
"priority": "Item priority",
"resolution": "Resolution",
"technology": "Technology",
"title": "Title"
},
"popup_seconds": "Status bar popup seconds",
"position": "Status bar position",
"positions": {
"bottom": "Positioned on the bottom",
"top": "Positioned on the top"
},
"style": "Status bar style",
"styles": {
"hover": "Hover status bar",
"hover-card": "Hover status bar (card-wide)",
"none": "No status bar",
"outside": "Outside status bar",
"overlay": "Overlay status bar",
"popup": "Popup status bar"
}
},
"view": {
"camera_select": "View for newly selected cameras",
"dark_mode": "Dark mode",
@@ -533,6 +548,8 @@
"performance_secondary": "Card performance options",
"profiles": "Configuration profiles",
"profiles_secondary": "Choose pre-configured sets of defaults",
"status_bar": "Status bar",
"status_bar_secondary": "Status bar look & feel options",
"timeline": "Timeline",
"timeline_secondary": "Event timeline options",
"upgrade": "Upgrade",
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
"duration_seconds": "Secondes pour afficher le titre de la fenêtre contextuelle (0 = toujours)",
"editor_label": "Contrôles de titre contextuel",
"mode": "Mode d'affichage du titre contextuel",
"modes": {
"none": "Pas d'affichage du titre",
"popup-bottom-left": "Popup en bas à gauche",
"popup-bottom-right": "Popup en bas à droite",
"popup-top-left": "Popup en haut à gauche",
"popup-top-right": "Popup en haut à droite"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "",
"scrubbing": ""
},
"status_bar": {
"height": "",
"items": {
"enabled": "",
"engine": "",
"live_provider": "",
"priority": "",
"resolution": "",
"technology": "",
"title": ""
},
"popup_seconds": "",
"position": "",
"positions": {
"bottom": "",
"top": ""
},
"style": "",
"styles": {
"hover": "",
"hover-card": "",
"none": "",
"outside": "",
"overlay": "",
"popup": ""
}
},
"view": {
"camera_select": "Afficher les caméras nouvellement sélectionnées",
"dark_mode": "Mode sombre",
@@ -533,6 +548,8 @@
"performance_secondary": "Options de performances de la carte",
"profiles": "",
"profiles_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Chronologie",
"timeline_secondary": "Options de chronologie des événements",
"upgrade": "Mise à niveau",
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
"duration_seconds": "Secondi per visualizzare il titolo popup (0 = per sempre)",
"editor_label": "Controlli titolo popup",
"mode": "Modalità di visualizzazione del titolo",
"modes": {
"none": "Nessuna visualizzazione del titolo",
"popup-bottom-left": "Popup in basso a sinistra",
"popup-bottom-right": "Popup in basso a destra",
"popup-top-left": "Popup in alto a sinistra",
"popup-top-right": "Popup in alto a destra"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "",
"scrubbing": ""
},
"status_bar": {
"height": "",
"items": {
"enabled": "",
"engine": "",
"live_provider": "",
"priority": "",
"resolution": "",
"technology": "",
"title": ""
},
"popup_seconds": "",
"position": "",
"positions": {
"bottom": "",
"top": ""
},
"style": "",
"styles": {
"hover": "",
"hover-card": "",
"none": "",
"outside": "",
"overlay": "",
"popup": ""
}
},
"view": {
"camera_select": "Visualizza per le telecamere appena selezionate",
"dark_mode": "Tema scuro",
@@ -533,6 +548,8 @@
"performance_secondary": "",
"profiles": "",
"profiles_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Timeline",
"timeline_secondary": "Opzioni della timeline degli eventi",
"upgrade": "Aggiornamento",
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
"duration_seconds": "Segundos para exibir o pop-up (0 = para sempre)",
"editor_label": "Controles do pop-up de título",
"mode": "Modo de exibição de título de mídia",
"modes": {
"none": "Sem exibição de título",
"popup-bottom-left": "Pop-up no canto inferior esquerdo",
"popup-bottom-right": "Pop-up no canto inferior direito",
"popup-top-left": "Pop-up no canto superior esquerdo",
"popup-top-right": "Pop-up no canto superior direito"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "",
"scrubbing": ""
},
"status_bar": {
"height": "",
"items": {
"enabled": "",
"engine": "",
"live_provider": "",
"priority": "",
"resolution": "",
"technology": "",
"title": ""
},
"popup_seconds": "",
"position": "",
"positions": {
"bottom": "",
"top": ""
},
"style": "",
"styles": {
"hover": "",
"hover-card": "",
"none": "",
"outside": "",
"overlay": "",
"popup": ""
}
},
"view": {
"camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro",
@@ -533,6 +548,8 @@
"performance_secondary": "Opções de desempenho do cartão",
"profiles": "",
"profiles_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Linha do tempo",
"timeline_secondary": "Opções do evento da linha do tempo",
"upgrade": "Upgrade",
+29 -12
View File
@@ -210,18 +210,6 @@
"seek-in-camera": "",
"seek-in-media": ""
}
},
"title": {
"duration_seconds": "Segundos para exibir o pop-up (0 = para sempre)",
"editor_label": "Editor de titulos",
"mode": "Modo de exibição de título de mídia",
"modes": {
"none": "Sem exibição de título",
"popup-bottom-left": "Pop-up no canto inferior esquerdo",
"popup-bottom-right": "Pop-up no canto inferior direito",
"popup-top-left": "Pop-up no canto superior esquerdo",
"popup-top-right": "Pop-up no canto superior direito"
}
}
},
"display": {
@@ -427,6 +415,33 @@
"low-performance": "",
"scrubbing": ""
},
"status_bar": {
"height": "",
"items": {
"enabled": "",
"engine": "",
"live_provider": "",
"priority": "",
"resolution": "",
"technology": "",
"title": ""
},
"popup_seconds": "",
"position": "",
"positions": {
"bottom": "",
"top": ""
},
"style": "",
"styles": {
"hover": "",
"hover-card": "",
"none": "",
"outside": "",
"overlay": "",
"popup": ""
}
},
"view": {
"camera_select": "Visualização de câmeras recém-selecionadas",
"dark_mode": "Modo escuro",
@@ -533,6 +548,8 @@
"performance_secondary": "",
"profiles": "",
"profiles_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Linha do tempo",
"timeline_secondary": "Opções do evento da linha do tempo",
"upgrade": "Actualização",
+4 -1
View File
@@ -98,7 +98,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
return html`
<img
@load=${(ev: Event) => {
dispatchMediaLoadedEvent(this, ev, { player: this });
dispatchMediaLoadedEvent(this, ev, {
player: this,
technology: ['mjpeg'],
});
}}
.src=${typeof this._connected == 'undefined' || this._connected
? computeMJPEGStreamUrl(this.stateObj)
+1
View File
@@ -125,6 +125,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
supportsPause: true,
hasAudio: mayHaveAudio(this._video),
},
technology: ['hls'],
});
}}
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
+1
View File
@@ -120,6 +120,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
supportsPause: true,
hasAudio: mayHaveAudio(this._video),
},
technology: ['webrtc'],
});
}}
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
+49 -17
View File
@@ -80,23 +80,6 @@ div.main.curve-bottom {
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
/* The 'hover' menu mode is styled applied outside of the menu itself */
frigate-card-menu[data-style*='hover'] {
z-index: 1;
transition: opacity 0.5s ease;
}
.main + frigate-card-menu[data-style*='hover'] {
opacity: 0;
}
frigate-card-menu[data-style='hover']:hover {
opacity: 1;
}
.main:hover + frigate-card-menu[data-style='hover-card'],
frigate-card-menu[data-style='hover-card']:hover {
opacity: 1;
}
ha-card {
display: flex;
flex-direction: column;
@@ -181,3 +164,52 @@ web-dialog::part(dialog) {
border-radius: 0px;
background: transparent;
}
/*************************************
* "Outside" style for menu/status bar
*************************************/
.outerlay[data-position='top'] {
border-top-left-radius: var(--ha-card-border-radius, 4px);
border-top-right-radius: var(--ha-card-border-radius, 4px);
}
.outerlay[data-position='bottom'] {
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
/****************
* Overlay styles
****************/
frigate-card-overlay {
z-index: 1;
}
/*******************
* Menu hover styles
*******************/
frigate-card-menu[data-style*='hover'] {
z-index: 4;
transition: opacity 0.5s ease;
opacity: 0;
}
frigate-card-menu[data-style*='hover']:hover,
.main:hover > frigate-card-overlay > frigate-card-menu[data-style='hover-card'] {
opacity: 1;
}
/*************************
* Status bar hover styles
*************************/
frigate-card-status-bar[data-style*='hover'] {
z-index: 3;
transition: opacity 0.5s ease;
opacity: 0;
}
frigate-card-status-bar[data-style*='hover']:hover,
.main:hover > frigate-card-overlay > frigate-card-status-bar[data-style='hover-card'] {
opacity: 1;
}
+9 -55
View File
@@ -5,9 +5,7 @@
--mdc-icon-button-size: var(--frigate-card-menu-button-size);
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
// Menu itself does not handle click events. Without this, in overlay mode the
// menu component prevents clicking on gallery items 'behind' the overlay.
pointer-events: none;
pointer-events: auto;
display: flex;
flex-direction: row;
@@ -45,52 +43,6 @@ div.opposing {
background: var(--secondary-background-color);
}
/************************************
* Match menu rounded corners to card
************************************/
:host([data-position='top']),
:host([data-position='left']) {
border-top-left-radius: var(--ha-card-border-radius, 4px);
}
:host([data-position='top']),
:host([data-position='right']) {
border-top-right-radius: var(--ha-card-border-radius, 4px);
}
:host([data-position='bottom']),
:host([data-position='left']) {
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
}
:host([data-position='bottom']),
:host([data-position='right']) {
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
/**************************************
* Positioning for absolute menu styles
**************************************/
:host(:not([data-style='outside'])[data-position='top']),
:host(:not([data-style='outside'])[data-position='left'][data-alignment='top']),
:host(:not([data-style='outside'])[data-position='right'][data-alignment='top']) {
top: 0px;
}
:host(:not([data-style='outside'])[data-position='bottom']),
:host(:not([data-style='outside'])[data-position='left'][data-alignment='bottom']),
:host(:not([data-style='outside'])[data-position='right'][data-alignment='bottom']) {
bottom: 0px;
}
:host(:not([data-style='outside'])[data-position='left']),
:host(:not([data-style='outside'])[data-position='top'][data-alignment='left']),
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='left']) {
left: 0px;
}
:host(:not([data-style='outside'])[data-position='right']),
:host(:not([data-style='outside'])[data-position='top'][data-alignment='right']),
:host(:not([data-style='outside'])[data-position='bottom'][data-alignment='right']) {
right: 0px;
}
/********************************************************
* Hack: Ensure host & div expand for column flex layouts
********************************************************/
@@ -156,12 +108,18 @@ div.opposing {
:host([data-style='overlay']),
:host([data-style*='hover']),
:host([data-style='hidden']) {
position: absolute;
overflow: hidden;
width: calc(var(--frigate-card-menu-button-size) + 6px);
height: calc(var(--frigate-card-menu-button-size) + 6px);
}
:host([data-style='overlay']),
:host([data-style*='hover']),
:host([data-style='hidden'][expanded]) {
overflow: visible;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
}
:host([data-style='overlay'][data-position='top']),
:host([data-style='overlay'][data-position='bottom']),
:host([data-style*='hover'][data-position='top']),
@@ -170,8 +128,6 @@ div.opposing {
:host([data-style='hidden'][data-position='bottom'][expanded]) {
width: 100%;
height: auto;
overflow: visible;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
}
:host([data-style='overlay'][data-position='left']),
@@ -180,8 +136,6 @@ div.opposing {
:host([data-style*='hover'][data-position='right']),
:host([data-style='hidden'][data-position='left'][expanded]),
:host([data-style='hidden'][data-position='right'][expanded]) {
height: 100%;
width: auto;
overflow: visible;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
height: 100%;
}
+66
View File
@@ -0,0 +1,66 @@
@use './button.scss';
:host {
position: absolute;
width: 100%;
height: 100%;
// Overlay itself does not handle click events.
pointer-events: none;
}
slot {
position: absolute;
display: block;
}
/***************
* Slot position
***************/
slot[name='top'] {
top: 0px;
}
slot[name='bottom'] {
bottom: 0px;
}
slot[name='left'] {
left: 0px;
}
slot[name='right'] {
right: 0px;
}
/***********
* Slot size
***********/
slot[name='top'],
slot[name='bottom'] {
width: 100%;
}
slot[name='left'],
slot[name='right'] {
height: 100%;
}
/*******************************
* Match rounded corners to card
*******************************/
slot[name='top'],
slot[name='left'] {
border-top-left-radius: var(--ha-card-border-radius, 4px);
}
slot[name='top'],
slot[name='right'] {
border-top-right-radius: var(--ha-card-border-radius, 4px);
}
slot[name='bottom'],
slot[name='left'] {
border-bottom-left-radius: var(--ha-card-border-radius, 4px);
}
slot[name='bottom'],
slot[name='right'] {
border-bottom-right-radius: var(--ha-card-border-radius, 4px);
}
+82
View File
@@ -0,0 +1,82 @@
@use './button.scss';
:host {
--mdc-icon-button-size: calc(var(--frigate-card-status-bar-height) - 6px);
--mdc-icon-size: calc(var(--mdc-icon-button-size) / 2);
display: block;
width: 100%;
pointer-events: auto;
opacity: 1;
transition: opacity 1s;
}
/***************
* Outside style
***************/
: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;
pointer-events: none;
}
/*********************
* Status bar contents
*********************/
.status {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
overflow: hidden;
width: 100%;
height: var(--frigate-card-status-bar-height);
}
.item {
display: inline-block;
margin: 3px;
padding: 3px;
align-content: center;
opacity: 0.7;
}
.item:first-child {
margin-left: 10px;
}
.item:last-child {
margin-right: 10px;
}
.item.expand {
flex: 1;
}
.item.action {
cursor: pointer;
}
img.item {
width: var(--mdc-icon-size, 24px);
height: var(--mdc-icon-size, 24px);
}
-29
View File
@@ -1,29 +0,0 @@
:host {
--paper-toast-background-color: rgba(0, 0, 0, 0.6);
--paper-toast-color: white;
pointer-events: none;
position: relative;
}
paper-toast {
max-width: unset;
min-width: unset;
display: flex;
align-items: center;
// Without this the paper-toast will consume vertical space before being
// opened, which causes the card to render blank space needlessly. It also
// won't work with 'display: none', it appears to need something with
// width/height properties even before being opened.
position: absolute;
}
paper-toast.paper-toast-open {
position: relative;
}
paper-toast img {
max-height: 24px;
padding-left: 10px;
}
+4
View File
@@ -31,9 +31,13 @@ export interface MediaLoadedCapabilities {
hasAudio?: boolean;
}
const MEDIA_TECHNOLOGY = ['hls', 'jpg', 'jsmpeg', 'mjpeg', 'mp4', 'mse', 'webrtc'];
export type MediaTechnology = (typeof MEDIA_TECHNOLOGY)[number];
export interface MediaLoadedInfo {
width: number;
height: number;
technology?: MediaTechnology[];
player?: FrigateCardMediaPlayer;
capabilities?: MediaLoadedCapabilities;
}
+3
View File
@@ -2,6 +2,7 @@ import {
FrigateCardMediaPlayer,
MediaLoadedCapabilities,
MediaLoadedInfo,
MediaTechnology,
} from '../types.js';
import { dispatchFrigateCardEvent } from './basic.js';
@@ -18,6 +19,7 @@ export function createMediaLoadedInfo(
options?: {
player?: FrigateCardMediaPlayer;
capabilities?: MediaLoadedCapabilities;
technology?: MediaTechnology[];
},
): MediaLoadedInfo | null {
let target: HTMLElement | EventTarget;
@@ -61,6 +63,7 @@ export function dispatchMediaLoadedEvent(
options?: {
player?: FrigateCardMediaPlayer;
capabilities?: MediaLoadedCapabilities;
technology?: MediaTechnology[];
},
): void {
const mediaLoadedInfo = createMediaLoadedInfo(source, options);