Initial version of status bar.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
},
|
||||
technology: ['jsmpeg'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> `
|
||||
: ``}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'] });
|
||||
}}
|
||||
/>`}
|
||||
`);
|
||||
|
||||
@@ -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'),
|
||||
];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -125,6 +125,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(this._video),
|
||||
},
|
||||
technology: ['hls'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
|
||||
@@ -120,6 +120,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(this._video),
|
||||
},
|
||||
technology: ['webrtc'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
|
||||
+49
-17
@@ -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
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user