feat: Allow user generated notifications (#2401)

This commit is contained in:
Dermot Duffy
2026-03-07 20:47:24 -08:00
committed by GitHub
parent 1d81e03b04
commit 24c6b98948
53 changed files with 939 additions and 471 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
import { ExpiringEqualityCache } from '../cache/expiring-cache';
import { SSLCiphers } from '../config/schema/cameras';
import { AdvancedCameraCardView } from '../config/schema/common/const';
import { InternalIcon } from '../config/schema/common/icon';
import { BaseQuery, QueryFilters, QuerySource } from '../query-source';
import { CapabilityKey, Endpoint, Icon } from '../types';
import { CapabilityKey, Endpoint } from '../types';
import { ViewMedia } from '../view/item';
// ====
@@ -118,7 +119,7 @@ export interface CapabilitySearchOptions {
export interface CameraManagerCameraMetadata {
title: string;
icon: Icon;
icon: InternalIcon;
// Engine icon is just a string since it will never be entity-derived.
engineIcon?: string;
+2 -2
View File
@@ -16,8 +16,8 @@ export class InfoAction extends AdvancedCameraCardAction<GeneralActionConfig> {
const controller = new MediaDetailsController();
controller.calculate(api.getCameraManager(), item);
api.getOverlayMessageManager().setMessage(
controller.getMessage({
api.getNotificationManager().setNotification(
controller.getNotification({
hass: api.getHASSManager().getHASS() ?? undefined,
viewItemManager: api.getViewItemManager(),
viewManagerEpoch: api.getViewManager().getEpoch(),
@@ -0,0 +1,10 @@
import { NotificationActionConfig } from '../../../config/schema/actions/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class NotificationAction extends AdvancedCameraCardAction<NotificationActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getNotificationManager().setNotification(this._action.notification);
}
}
+3
View File
@@ -25,6 +25,7 @@ import { MoreInfoAction } from './actions/more-info';
import { MuteAction } from './actions/mute';
import { NavigateAction } from './actions/navigate';
import { NoneAction } from './actions/none';
import { NotificationAction } from './actions/notification';
import { PauseAction } from './actions/pause';
import { PerformActionAction } from './actions/perform-action';
import { PIPAction } from './actions/pip';
@@ -163,6 +164,8 @@ export class ActionFactory {
return new PTZControlsAction(context, action, options?.config);
case 'log':
return new LogAction(context, action, options?.config);
case 'notification':
return new NotificationAction(context, action, options?.config);
case 'status_bar':
return new StatusBarAction(context, action, options?.config);
case 'reload':
+8 -2
View File
@@ -28,6 +28,7 @@ export class ConfigManager {
private _overriddenConfig: AdvancedCameraCardConfig | null = null;
private _rawConfig: RawAdvancedCameraCardConfig | null = null;
private _cardWideConfig: CardWideConfig | null = null;
private _upgradeable = false;
private _overridesManager = new OverridesManager(() => this._processOverrideConfig());
constructor(api: CardConfigAPI) {
@@ -54,17 +55,22 @@ export class ConfigManager {
return this._rawConfig;
}
public isUpgradeable(): boolean {
return this._upgradeable;
}
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
if (!inputConfig) {
throw new Error(localize('error.invalid_configuration'));
}
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
this._upgradeable = isConfigUpgradeable(inputConfig);
if (!parseResult.success) {
const configUpgradeable = isConfigUpgradeable(inputConfig);
const hint = getParseError(parseResult.error);
let upgradeMessage = '';
if (configUpgradeable) {
if (this._upgradeable) {
upgradeMessage = `${localize('error.upgrade_available')}. `;
}
throw new Error(
+6 -6
View File
@@ -31,7 +31,7 @@ import { MediaLoadedInfoManager } from './media-info-manager';
import { MediaPlayerManager } from './media-player-manager';
import { MessageManager } from './message-manager';
import { MicrophoneManager } from './microphone-manager';
import { OverlayMessageManager } from './overlay-message-manager';
import { NotificationManager } from './notification-manager';
import { PIPManager } from './pip-manager';
import { QueryStringManager } from './query-string-manager';
import { StatusBarItemManager } from './status-bar-item-manager';
@@ -58,7 +58,7 @@ import {
CardMediaPlayerAPI,
CardMessageAPI,
CardMicrophoneAPI,
CardOverlayMessageAPI,
CardNotificationAPI,
CardPIPAPI,
CardQueryStringAPI,
CardStyleAPI,
@@ -90,7 +90,7 @@ export class CardController
CardMediaPlayerAPI,
CardMessageAPI,
CardMicrophoneAPI,
CardOverlayMessageAPI,
CardNotificationAPI,
CardQueryStringAPI,
CardStyleAPI,
CardTriggersAPI,
@@ -125,7 +125,7 @@ export class CardController
private _mediaPlayerManager = new MediaPlayerManager(this);
private _messageManager = new MessageManager(this);
private _microphoneManager = new MicrophoneManager(this);
private _overlayMessageManager = new OverlayMessageManager(this);
private _notificationManager = new NotificationManager(this);
private _pipManager = new PIPManager(this);
private _queryStringManager = new QueryStringManager(this);
private _statusBarItemManager = new StatusBarItemManager(this);
@@ -252,8 +252,8 @@ export class CardController
this._microphoneManager = new MicrophoneManager(this);
}
public getOverlayMessageManager(): OverlayMessageManager {
return this._overlayMessageManager;
public getNotificationManager(): NotificationManager {
return this._notificationManager;
}
public getPIPManager(): PIPManager {
@@ -0,0 +1,33 @@
import { Notification } from '../config/schema/actions/types';
import { CardNotificationAPI } from './types';
export class NotificationManager {
private _notification: Notification | null = null;
private _api: CardNotificationAPI;
constructor(api: CardNotificationAPI) {
this._api = api;
}
public getNotification(): Notification | null {
return this._notification;
}
public hasNotification(): boolean {
return this._notification !== null;
}
// Also used to replace the current notification in-place (e.g. to refresh
// control state after a toggle action).
public setNotification(notification: Notification): void {
this._notification = notification;
this._api.getCardElementManager().update();
}
public reset(): void {
if (this._notification) {
this._notification = null;
this._api.getCardElementManager().update();
}
}
}
@@ -1,31 +0,0 @@
import { OverlayMessage } from '../types';
import { CardOverlayMessageAPI } from './types';
export class OverlayMessageManager {
private _message: OverlayMessage | null = null;
private _api: CardOverlayMessageAPI;
constructor(api: CardOverlayMessageAPI) {
this._api = api;
}
public getMessage(): OverlayMessage | null {
return this._message;
}
public hasMessage(): boolean {
return this._message !== null;
}
public setMessage(message: OverlayMessage): void {
this._message = message;
this._api.getCardElementManager().update();
}
public reset(): void {
if (this._message) {
this._message = null;
this._api.getCardElementManager().update();
}
}
}
@@ -2,7 +2,9 @@ import { isEqual } from 'lodash-es';
import { CameraManager } from '../camera-manager/manager';
import { StatusBarItem } from '../config/schema/actions/types';
import { StatusBarConfig } from '../config/schema/status-bar';
import { localize } from '../localize/localize';
import { MediaLoadedInfo } from '../types';
import { createNotificationAction } from '../utils/action';
import { View } from '../view/view';
import { CardStatusBarAPI } from './types';
@@ -42,6 +44,7 @@ export class StatusBarItemManager {
cameraManager?: CameraManager | null;
view?: View | null;
mediaLoadedInfo?: MediaLoadedInfo | null;
isUpgradeable?: boolean;
}): StatusBarItem[] {
const cameraMetadata = options?.view?.camera
? options?.cameraManager?.getCameraMetadata(options.view.camera)
@@ -124,6 +127,27 @@ export class StatusBarItemManager {
},
]
: []),
...(options?.isUpgradeable
? [
{
type: 'custom:advanced-camera-card-status-bar-icon' as const,
icon: 'mdi:update',
severity: 'medium' as const,
actions: {
tap_action: createNotificationAction({
heading: {
text: localize('notification.upgrade.heading'),
icon: 'mdi:update',
severity: 'medium',
},
text: localize('notification.upgrade.text'),
}),
},
...options?.statusConfig?.items.upgrade,
},
]
: []),
...this._dynamicItems,
];
}
+3 -3
View File
@@ -22,7 +22,7 @@ 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 { OverlayMessageManager } from './overlay-message-manager';
import type { NotificationManager } from './notification-manager';
import type { PIPManager } from './pip-manager';
import type { QueryStringManager } from './query-string-manager';
import type { StatusBarItemManager } from './status-bar-item-manager';
@@ -54,7 +54,7 @@ export interface CardActionsAPI {
getMediaPlayerManager(): MediaPlayerManager;
getMessageManager(): MessageManager;
getMicrophoneManager(): MicrophoneManager;
getOverlayMessageManager(): OverlayMessageManager;
getNotificationManager(): NotificationManager;
getPIPManager(): PIPManager;
getStatusBarItemManager(): StatusBarItemManager;
getTriggersManager(): TriggersManager;
@@ -257,7 +257,7 @@ export interface CardMessageAPI {
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
}
export interface CardOverlayMessageAPI {
export interface CardNotificationAPI {
getCardElementManager(): CardElementManager;
}
+11 -11
View File
@@ -16,7 +16,7 @@ import './components/menu.js';
import { AdvancedCameraCardMenu } from './components/menu.js';
import './components/message.js';
import { renderMessage } from './components/message.js';
import './components/overlay-message.js';
import './components/notification.js';
import './components/overlay.js';
import { AdvancedCameraCardOverlay } from './components/overlay.js';
import './components/status-bar';
@@ -32,7 +32,7 @@ import { REPO_URL } from './const.js';
import { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
import { localize } from './localize/localize.js';
import cardStyle from './scss/card.scss';
import { MediaLoadedInfo, Message, OverlayMessage } from './types.js';
import { MediaLoadedInfo, Message } from './types.js';
import { hasAction } from './utils/action.js';
import { getReleaseVersion } from './utils/diagnostics';
@@ -288,6 +288,7 @@ class AdvancedCameraCard extends LitElement {
cameraManager: this._controller.getCameraManager(),
view: this._controller.getViewManager().getView(),
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
isUpgradeable: this._controller.getConfigManager().isUpgradeable(),
})}
.config=${this._config.status_bar}
></advanced-camera-card-status-bar>
@@ -374,11 +375,8 @@ class AdvancedCameraCard extends LitElement {
() => this.requestUpdate() /* Refresh play/pause menu button */
}
@advanced-camera-card:focus=${() => this.focus()}
@advanced-camera-card:overlay-message:show=${(
ev: CustomEvent<OverlayMessage>,
) => this._controller.getOverlayMessageManager().setMessage(ev.detail)}
@advanced-camera-card:overlay-message:dismiss=${() =>
this._controller.getOverlayMessageManager().reset()}
@advanced-camera-card:notification:dismiss=${() =>
this._controller.getNotificationManager().reset()}
>
${showLoading
? html`<advanced-camera-card-loading
@@ -459,10 +457,12 @@ class AdvancedCameraCard extends LitElement {
>
</advanced-camera-card-elements>`
: ``}
${this._controller.getOverlayMessageManager().getMessage()
? html`<advanced-camera-card-overlay-message
.message=${this._controller.getOverlayMessageManager().getMessage()}
></advanced-camera-card-overlay-message>`
${this._controller.getNotificationManager().getNotification()
? html`<advanced-camera-card-notification
.notification=${this._controller
.getNotificationManager()
.getNotification()}
></advanced-camera-card-notification>`
: ''}
</ha-card>`,
);
+1 -1
View File
@@ -3,9 +3,9 @@ import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
import tplinkSVG from '../camera-manager/tplink/assets/tplink.svg';
import { Icon } from '../config/schema/common/icon';
import { HomeAssistant } from '../ha/types';
import irisSVG from '../images/iris.svg';
import { Icon } from '../types';
export class IconController {
public getCustomIcon(icon?: Icon): string | null {
+85 -71
View File
@@ -3,9 +3,14 @@ import { CameraManager } from '../../camera-manager/manager';
import { CameraManagerCameraMetadata } from '../../camera-manager/types';
import { ViewItemManager } from '../../card-controller/view/item-manager';
import { ViewManagerEpoch } from '../../card-controller/view/types';
import {
Notification,
NotificationControl,
NotificationDetail,
} from '../../config/schema/actions/types';
import { HomeAssistant } from '../../ha/types';
import { localize } from '../../localize/localize';
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../../types';
import { createInternalCallbackAction } from '../../utils/action';
import { getDurationString, prettifyTitle } from '../../utils/basic';
import {
downloadMedia,
@@ -17,7 +22,7 @@ import { ViewItem } from '../../view/item';
import { ViewItemClassifier } from '../../view/item-classifier';
import { ViewItemCapabilities } from '../../view/types';
export interface OverlayControlsContext {
export interface NotificationControlsContext {
hass?: HomeAssistant;
viewItemManager?: ViewItemManager;
viewManagerEpoch?: ViewManagerEpoch;
@@ -29,8 +34,8 @@ export interface OverlayControlsContext {
}
export class MediaDetailsController {
private _details: MetadataField[] = [];
private _heading: MetadataField | null = null;
private _details: NotificationDetail[] = [];
private _heading: NotificationDetail | null = null;
private _item: ViewItem | null = null;
public calculate(
@@ -61,7 +66,7 @@ export class MediaDetailsController {
const score = rawScore ? (rawScore * 100).toFixed(2) + '%' : null;
this._heading = whatWithTags
? { title: `${whatWithTags}${score ? ` ${score}` : ''}` }
? { text: `${whatWithTags}${score ? ` ${score}` : ''}` }
: null;
return;
}
@@ -72,13 +77,13 @@ export class MediaDetailsController {
this._heading = title
? {
title: title,
emphasis: severity ?? undefined,
hint:
text: title,
severity: severity ?? undefined,
tooltip:
localize('common.severity') +
': ' +
localize('common.severities.' + severity),
icon: { icon: 'mdi:circle-medium' },
icon: 'mdi:circle-medium',
}
: null;
return;
@@ -86,7 +91,7 @@ export class MediaDetailsController {
if (cameraMetadata?.title) {
this._heading = {
title: cameraMetadata.title,
text: cameraMetadata.title,
};
return;
}
@@ -121,54 +126,54 @@ export class MediaDetailsController {
...(startTime
? [
{
hint: localize('thumbnail.start'),
icon: { icon: 'mdi:calendar-clock-outline' },
title: format(startTime, 'yyyy-MM-dd HH:mm:ss'),
tooltip: localize('thumbnail.start'),
icon: 'mdi:calendar-clock-outline',
text: format(startTime, 'yyyy-MM-dd HH:mm:ss'),
},
]
: []),
...(duration || inProgress
? [
{
hint: localize('thumbnail.duration'),
icon: { icon: 'mdi:clock-outline' },
title: `${duration ?? ''}${duration && inProgress ? ' ' : ''}${inProgress ?? ''}`,
tooltip: localize('thumbnail.duration'),
icon: 'mdi:clock-outline',
text: `${duration ?? ''}${duration && inProgress ? ' ' : ''}${inProgress ?? ''}`,
},
]
: []),
...(cameraMetadata?.title
? [
{
hint: localize('thumbnail.camera'),
title: cameraMetadata.title,
icon: { icon: 'mdi:cctv' },
tooltip: localize('thumbnail.camera'),
text: cameraMetadata.title,
icon: 'mdi:cctv',
},
]
: []),
...(where
? [
{
hint: localize('thumbnail.where'),
title: where,
icon: { icon: 'mdi:map-marker-outline' },
tooltip: localize('thumbnail.where'),
text: where,
icon: 'mdi:map-marker-outline',
},
]
: []),
...(tags
? [
{
hint: localize('thumbnail.tag'),
title: tags,
icon: { icon: 'mdi:tag' },
tooltip: localize('thumbnail.tag'),
text: tags,
icon: 'mdi:tag',
},
]
: []),
...(seekString
? [
{
hint: localize('thumbnail.seek'),
title: seekString,
icon: { icon: 'mdi:clock-fast' },
tooltip: localize('thumbnail.seek'),
text: seekString,
icon: 'mdi:clock-fast',
},
]
: []),
@@ -183,10 +188,10 @@ export class MediaDetailsController {
...(includeTitle && itemTitle
? [
{
title: itemTitle,
text: itemTitle,
...(details.length > 0 && {
icon: { icon: 'mdi:rename' },
hint: localize('thumbnail.title'),
icon: 'mdi:rename',
tooltip: localize('thumbnail.title'),
}),
},
]
@@ -195,20 +200,15 @@ export class MediaDetailsController {
];
}
public getHeading(): MetadataField | null {
public getHeading(): NotificationDetail | null {
return this._heading;
}
public getDetails(): MetadataField[] {
public getDetails(): NotificationDetail[] {
return this._details;
}
/**
* Get an overlay message for the item.
* @param context Optional context to include controls.
* @returns An OverlayMessage.
*/
public getMessage(context?: OverlayControlsContext): OverlayMessage {
public getNotification(context?: NotificationControlsContext): Notification {
return {
heading: this._heading ?? undefined,
controls: context ? this._getControls(context) : undefined,
@@ -219,8 +219,8 @@ export class MediaDetailsController {
};
}
private _getControls(context: OverlayControlsContext): OverlayMessageControl[] {
const controls: OverlayMessageControl[] = [];
private _getControls(context: NotificationControlsContext): NotificationControl[] {
const controls: NotificationControl[] = [];
const item = this._item;
if (!item) {
@@ -230,57 +230,71 @@ export class MediaDetailsController {
if (ViewItemClassifier.isReview(item)) {
const isReviewed = item.isReviewed();
controls.push({
title: isReviewed
tooltip: isReviewed
? localize('common.set_reviews.unreviewed')
: localize('common.set_reviews.reviewed'),
icon: { icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline' },
callback: async () => {
const success = await toggleReviewed(
item,
context.viewItemManager,
context.viewManagerEpoch,
context.filterReviewed,
);
return success ? this.getMessage(context) : null;
icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline',
actions: {
tap_action: createInternalCallbackAction(async (api) => {
const success = await toggleReviewed(
item,
context.viewItemManager,
context.viewManagerEpoch,
context.filterReviewed,
);
if (success) {
api
.getNotificationManager()
.setNotification(this.getNotification(context));
}
}),
},
dismiss: false,
});
}
if (context.capabilities?.canFavorite && ViewItemClassifier.isMedia(item)) {
const isFavorite = item.isFavorite();
controls.push({
title: localize('thumbnail.retain_indefinitely'),
icon: { icon: isFavorite ? 'mdi:star' : 'mdi:star-outline' },
emphasis: isFavorite ? 'medium' : undefined,
callback: async () => {
const success = await toggleFavorite(item, context.viewItemManager);
return success ? this.getMessage(context) : null;
tooltip: localize('thumbnail.retain_indefinitely'),
icon: isFavorite ? 'mdi:star' : 'mdi:star-outline',
severity: isFavorite ? 'medium' : undefined,
actions: {
tap_action: createInternalCallbackAction(async (api) => {
const success = await toggleFavorite(item, context.viewItemManager);
if (success) {
api
.getNotificationManager()
.setNotification(this.getNotification(context));
}
}),
},
dismiss: false,
});
}
if (context.capabilities?.canDownload && item.getID()) {
controls.push({
title: localize('thumbnail.download'),
icon: { icon: 'mdi:download' },
callback: async () => {
await downloadMedia(item, context.viewItemManager);
// Close overlay message after download.
return null;
tooltip: localize('thumbnail.download'),
icon: 'mdi:download',
dismiss: true,
actions: {
tap_action: createInternalCallbackAction(async () => {
await downloadMedia(item, context.viewItemManager);
}),
},
});
}
if (ViewItemClassifier.supportsTimeline(item) && context.viewManagerEpoch) {
controls.push({
title: localize('thumbnail.timeline'),
icon: { icon: 'mdi:target' },
callback: () => {
navigateToTimeline(item, context.viewManagerEpoch);
// Close overlay after timeline navigation
return null;
tooltip: localize('thumbnail.timeline'),
icon: 'mdi:target',
dismiss: true,
actions: {
tap_action: createInternalCallbackAction(async () => {
navigateToTimeline(item, context.viewManagerEpoch);
}),
},
});
}
+2 -2
View File
@@ -8,9 +8,9 @@ import {
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { IconController } from '../components-lib/icon-controller';
import { InternalIcon } from '../config/schema/common/icon.js';
import { HomeAssistant } from '../ha/types';
import iconStyle from '../scss/icon.scss';
import { Icon } from '../types.js';
import { contentsChanged } from '../utils/basic.js';
@customElement('advanced-camera-card-icon')
@@ -19,7 +19,7 @@ export class AdvancedCameraCardIcon extends LitElement {
public hass?: HomeAssistant;
@property({ attribute: false, hasChanged: contentsChanged })
public icon?: Icon;
public icon?: InternalIcon;
// Note: This attribute will allow non-active entity state styles (e.g. 'off',
// 'unavailable') to be overriden from outside the icon itself. This is useful
+1 -1
View File
@@ -2,9 +2,9 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { NextPreviousControlConfig } from '../config/schema/common/controls/next-previous.js';
import { Icon } from '../config/schema/common/icon.js';
import { HomeAssistant } from '../ha/types.js';
import controlStyle from '../scss/next-previous-control.scss';
import { Icon } from '../types.js';
import { renderTask } from '../utils/task.js';
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
@@ -2,17 +2,29 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, Ref, ref } from 'lit/directives/ref.js';
import overlayMessageStyle from '../scss/overlay-message.scss';
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../types.js';
import { dispatchDismissOverlayMessageEvent } from '../utils/overlay-message.js';
import { actionHandler } from '../action-handler-directive.js';
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request';
import {
Notification,
NotificationControl,
NotificationDetail,
} from '../config/schema/actions/types.js';
import notificationStyle from '../scss/notification.scss';
import {
getActionConfigGivenAction,
hasAction,
stopEventFromActivatingCardWideActions,
} from '../utils/action.js';
import { arrayify } from '../utils/basic.js';
import { dispatchDismissNotificationEvent } from '../utils/notification.js';
import './icon.js';
@customElement('advanced-camera-card-overlay-message')
export class AdvancedCameraCardOverlayMessage extends LitElement {
@customElement('advanced-camera-card-notification')
export class AdvancedCameraCardNotification extends LitElement {
@property({ attribute: false })
public message: OverlayMessage | null = null;
public notification: Notification | null = null;
private _refMessage: Ref<HTMLElement> = createRef();
private _refNotification: Ref<HTMLElement> = createRef();
public connectedCallback(): void {
super.connectedCallback();
@@ -29,20 +41,20 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
}
protected render(): TemplateResult | void {
if (!this.message) {
if (!this.notification) {
return;
}
const heading = this.message.heading;
const details = this.message.details ?? [];
const text = this.message.text;
const controls = this.message.controls ?? [];
const heading = this.notification.heading;
const details = this.notification.details ?? [];
const text = this.notification.text;
const controls = this.notification.controls ?? [];
return html`
<div class="backdrop" @click=${this._dismiss}></div>
<div
class="message"
${ref(this._refMessage)}
class="notification"
${ref(this._refNotification)}
@animationend=${this._handleAnimationEnd}
>
<div class="details">
@@ -64,60 +76,70 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
`;
}
private _renderControl(control: OverlayMessageControl): TemplateResult {
const emphasisClass = control.emphasis ? `emphasis-${control.emphasis}` : '';
private _renderControl(control: NotificationControl): TemplateResult {
const severityClass = control.severity ? `severity-${control.severity}` : '';
return html`
<div
class="control ${emphasisClass}"
title=${control.title}
@click=${async () => this._handleControlClick(control)}
class="control ${severityClass}"
title=${control.tooltip ?? ''}
.actionHandler=${actionHandler({
hasHold: hasAction(control.actions?.hold_action),
hasDoubleClick: hasAction(control.actions?.double_tap_action),
})}
@action=${(ev: CustomEvent) => this._handleControlAction(ev, control)}
>
${control.icon
? html`<advanced-camera-card-icon
.icon=${control.icon}
.icon=${{ icon: control.icon }}
></advanced-camera-card-icon>`
: ''}
</div>
`;
}
private async _handleControlClick(control: OverlayMessageControl): Promise<void> {
const result = await control.callback();
if (result === null) {
// null = close the message
private _handleControlAction(
ev: CustomEvent<{ action: string }>,
control: NotificationControl,
): void {
stopEventFromActivatingCardWideActions(ev);
const action = getActionConfigGivenAction(ev.detail.action, control.actions);
if (action) {
dispatchActionExecutionRequest(this, {
actions: arrayify(action),
});
}
if (control.dismiss !== false) {
this._dismiss();
} else {
// Updated message = keep open and refresh
this.message = result;
}
}
private _renderDetail(detail: MetadataField, isHeading = false): TemplateResult {
private _renderDetail(detail: NotificationDetail, isHeading = false): TemplateResult {
const classes = {
detail: true,
heading: isHeading,
[`emphasis-${detail.emphasis}`]: !!detail.emphasis,
[`severity-${detail.severity}`]: !!detail.severity,
};
return html`
<div class="${classMap(classes)}">
${detail.icon
? html`<advanced-camera-card-icon
title=${detail.hint ?? ''}
.icon=${detail.icon}
title=${detail.tooltip ?? ''}
.icon=${{ icon: detail.icon }}
></advanced-camera-card-icon>`
: ''}
<span title=${detail.title}>${detail.title}</span>
<span title=${detail.text}>${detail.text}</span>
</div>
`;
}
private _dismiss = (): void => {
this._refMessage.value?.classList.add('exiting');
this._refNotification.value?.classList.add('exiting');
};
private _handleAnimationEnd = (ev: AnimationEvent): void => {
if (ev.animationName === 'slideDown') {
dispatchDismissOverlayMessageEvent(this);
dispatchDismissNotificationEvent(this);
}
};
@@ -136,12 +158,12 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
};
static get styles(): CSSResultGroup {
return unsafeCSS(overlayMessageStyle);
return unsafeCSS(notificationStyle);
}
}
declare global {
interface HTMLElementTagNameMap {
'advanced-camera-card-overlay-message': AdvancedCameraCardOverlayMessage;
'advanced-camera-card-notification': AdvancedCameraCardNotification;
}
}
+2 -2
View File
@@ -8,6 +8,7 @@ import {
} from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { InternalIcon } from '../../config/schema/common/icon.js';
import { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
import { computeDomain } from '../../ha/compute-domain.js';
@@ -17,7 +18,6 @@ import { isHassDifferent } from '../../ha/is-hass-different.js';
import { EntityRegistryManager } from '../../ha/registry/entity/types.js';
import { HomeAssistant } from '../../ha/types.js';
import menuButtonStyle from '../../scss/menu-button.scss';
import { Icon } from '../../types.js';
import { createSelectOptionAction } from '../../utils/action.js';
import '../icon.js';
import './index.js';
@@ -37,7 +37,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
private _optionTitles?: Record<string, string>;
private _generatedSubmenuItems?: MenuSubmenuItem[];
private _generatedIcon?: Icon;
private _generatedIcon?: InternalIcon;
protected shouldUpdate(changedProps: PropertyValues): boolean {
// No need to update the submenu unless the select entity has changed.
+10 -6
View File
@@ -8,11 +8,12 @@ import {
} from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { CameraManager } from '../../camera-manager/manager';
import { MediaDetailsController } from '../../components-lib/media/details-controller';
import { NotificationDetail } from '../../config/schema/actions/types';
import { HomeAssistant } from '../../ha/types';
import thumbnailDetailsStyle from '../../scss/thumbnail-details.scss';
import { MetadataField } from '../../types';
import { ViewItem } from '../../view/item';
import '../icon';
@@ -42,7 +43,10 @@ export class AdvancedCameraCardThumbnailDetails extends LitElement {
const heading = this._controller.getHeading();
const details = this._controller.getDetails();
const renderDetail = (detail: MetadataField, heading = false): TemplateResult => {
const renderDetail = (
detail: NotificationDetail,
heading = false,
): TemplateResult => {
return html`<div
class=${classMap({
heading,
@@ -50,12 +54,12 @@ export class AdvancedCameraCardThumbnailDetails extends LitElement {
>
${detail.icon
? html` <advanced-camera-card-icon
severity=${detail.emphasis}
title=${detail.hint ?? ''}
.icon=${detail.icon}
severity=${ifDefined(detail.severity)}
title=${detail.tooltip ?? ''}
.icon=${{ icon: detail.icon }}
></advanced-camera-card-icon>`
: ''}
<span title=${detail.title}>${detail.title}</span>
<span title=${detail.text}>${detail.text}</span>
</div>`;
};
+14 -8
View File
@@ -9,24 +9,27 @@ import {
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../../../camera-manager/manager';
import { dispatchActionExecutionRequest } from '../../../card-controller/actions/utils/execution-request';
import { ViewItemManager } from '../../../card-controller/view/item-manager';
import { ViewManagerEpoch } from '../../../card-controller/view/types';
import {
MediaDetailsController,
OverlayControlsContext,
NotificationControlsContext,
} from '../../../components-lib/media/details-controller';
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
import { HomeAssistant } from '../../../ha/types';
import { localize } from '../../../localize/localize';
import thumbnailFeatureStyle from '../../../scss/thumbnail-feature.scss';
import { stopEventFromActivatingCardWideActions } from '../../../utils/action';
import {
createNotificationAction,
stopEventFromActivatingCardWideActions,
} from '../../../utils/action';
import {
downloadMedia,
navigateToTimeline,
toggleFavorite,
toggleReviewed,
} from '../../../utils/media-actions';
import { dispatchShowOverlayMessageEvent } from '../../../utils/overlay-message';
import { ViewItem } from '../../../view/item';
import { ViewItemClassifier } from '../../../view/item-classifier';
import '../../icon.js';
@@ -80,7 +83,7 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
}
}
private _getControlContext(): OverlayControlsContext {
private _getControlContext(): NotificationControlsContext {
return {
hass: this.hass,
viewItemManager: this.viewItemManager,
@@ -207,10 +210,13 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
stopEventFromActivatingCardWideActions(ev);
const detailsController = new MediaDetailsController();
detailsController.calculate(this.cameraManager, this.item);
dispatchShowOverlayMessageEvent(
this,
detailsController.getMessage(this._getControlContext()),
);
dispatchActionExecutionRequest(this, {
actions: [
createNotificationAction(
detailsController.getNotification(this._getControlContext()),
),
],
});
}}
></advanced-camera-card-icon>`
: ''}
+53 -2
View File
@@ -19,9 +19,26 @@ import { substreamSelectActionConfigSchema } from './custom/substream-select';
import { viewActionConfigSchema } from './custom/view';
import { stockActionSchema } from './stock/types';
// Provide a manual type definition to avoid the `any` that would be created by
// the lazy() evaluation below.
// ============================================================================
// Notification and Status Bar action schemas are co-located here because their
// content schemas reference actionConfigSchema (creating a circular dep).
// Each uses z.lazy + a manual type annotation to break the cycle and preserve
// correct type inference.
// See: https://zod.dev/?id=recursive-types
// ============================================================================
export type NotificationActionConfig = z.infer<
typeof advancedCameraCardCustomActionsBaseSchema
> & {
advanced_camera_card_action: 'notification';
notification: Notification;
};
export const notificationActionConfigSchema: z.ZodSchema<NotificationActionConfig> =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal('notification'),
notification: z.lazy(() => notificationSchema),
});
export type StatusBarActionConfig = z.infer<
typeof advancedCameraCardCustomActionsBaseSchema
> & {
@@ -46,6 +63,7 @@ const advancedCameraCardCustomActionSchema = z.union([
internalCallbackActionConfigSchema,
logActionConfigSchema,
mediaPlayerActionConfigSchema,
notificationActionConfigSchema,
ptzActionConfigSchema,
ptzControlsActionConfigSchema,
ptzDigitalActionConfigSchema,
@@ -66,6 +84,7 @@ export const actionConfigSchema = z.union([
advancedCameraCardCustomActionSchema,
]);
export type ActionConfig = z.infer<typeof actionConfigSchema>;
export const actionsBaseSchema = z
.object({
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
@@ -90,6 +109,38 @@ export const actionsSchema = z.object({
actions: actionsBaseSchema.optional(),
});
// ============================================================================
// Notification Elements
//
// Note: Notification schemas are defined here (after actionsBaseSchema) so
// controls can directly reference actionsBaseSchema without z.lazy.
// ============================================================================
const notificationBaseSchema = z.object({
icon: z.string().optional(),
tooltip: z.string().optional(),
severity: severitySchema.optional(),
});
export const notificationDetailSchema = notificationBaseSchema.extend({
text: z.string(),
});
export type NotificationDetail = z.infer<typeof notificationDetailSchema>;
export const notificationControlSchema = notificationBaseSchema.extend({
actions: actionsBaseSchema.optional(),
dismiss: z.boolean().default(true),
});
export type NotificationControl = z.infer<typeof notificationControlSchema>;
export const notificationSchema = z.object({
heading: notificationDetailSchema.optional(),
controls: notificationControlSchema.array().optional(),
details: notificationDetailSchema.array().optional(),
text: z.string().optional(),
});
export type Notification = z.infer<typeof notificationSchema>;
// ============================================================================
// Status Bar Elements
//
+18
View File
@@ -0,0 +1,18 @@
import { z } from 'zod';
export const iconSchema = z.object({
// MDI icon name (e.g. 'mdi:star').
icon: z.string().optional(),
// HA entity whose icon will be used when `icon` is not set.
entity: z.string().optional(),
// Whether to tint the icon color based on the entity's state.
stateColor: z.boolean().optional(),
});
export type Icon = z.infer<typeof iconSchema>;
// Extended internally to include a fallback icon that is not user-configurable.
export interface InternalIcon extends Icon {
fallback?: string;
}
+2
View File
@@ -26,6 +26,7 @@ export const statusBarConfigDefault = {
severity: statusBarItemDefault,
technology: statusBarItemDefault,
title: statusBarItemDefault,
upgrade: statusBarItemDefault,
},
position: 'bottom' as const,
style: 'popup' as const,
@@ -53,6 +54,7 @@ export const statusBarConfigSchema = z
),
severity: statusBarItemBaseSchema.default(statusBarConfigDefault.items.severity),
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
upgrade: statusBarItemBaseSchema.default(statusBarConfigDefault.items.upgrade),
})
.default(statusBarConfigDefault.items),
})
+1
View File
@@ -3110,6 +3110,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
${this._renderStatusBarItem('resolution') /* */}
${this._renderStatusBarItem('technology') /* */}
${this._renderStatusBarItem('engine') /* */}
${this._renderStatusBarItem('upgrade') /* */}
</div>
`
: ''}
+9 -1
View File
@@ -6,6 +6,7 @@
"common": {
"advanced_camera_card": "Advanced Camera Card",
"advanced_camera_card_description": "An Advanced Camera Card",
"dismiss": "Dismiss",
"folder": "Folder",
"live": "Live",
"no_folder": "No folder to display",
@@ -557,7 +558,8 @@
"priority": "Item priority",
"resolution": "Resolution",
"technology": "Technology",
"title": "Title"
"title": "Title",
"upgrade": "Configuration upgrade notification"
},
"popup_seconds": "Status bar popup seconds",
"position": "Status bar position",
@@ -821,6 +823,12 @@
"media_viewer": {
"unseekable": "Seek time not found in media"
},
"notification": {
"upgrade": {
"heading": "Configuration upgrade available",
"text": "A configuration upgrade is available. To upgrade, edit this card (Dashboard pencil icon \u2192 Three-dot menu \u2192 Edit) and click the 'Automatic Upgrade' button in the card editor."
}
},
"thumbnail": {
"camera": "Camera",
"download": "Download media",
@@ -8,7 +8,7 @@
align-items: flex-end;
padding: 24px;
pointer-events: none;
z-index: $z-index-overlay-message;
z-index: $z-index-notification;
}
.backdrop {
@@ -17,7 +17,7 @@
pointer-events: auto;
}
.message {
.notification {
position: relative;
display: flex;
flex-direction: row;
@@ -30,23 +30,22 @@
padding-right: 48px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--advanced-camera-card-overlay-message-scrollbar-color)
transparent;
scrollbar-color: var(--advanced-camera-card-notification-scrollbar-color) transparent;
// Glassmorphism background
background: var(--advanced-camera-card-overlay-message-background);
backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
-webkit-backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
background: var(--advanced-camera-card-notification-background);
backdrop-filter: var(--advanced-camera-card-notification-backdrop-filter);
-webkit-backdrop-filter: var(--advanced-camera-card-notification-backdrop-filter);
// Borders & Edge highlights
border: var(--advanced-camera-card-overlay-message-border);
border-top: var(--advanced-camera-card-overlay-message-border-top);
border: var(--advanced-camera-card-notification-border);
border-top: var(--advanced-camera-card-notification-border-top);
border-radius: var(--advanced-camera-card-border-radius-final);
// Shadows
box-shadow: var(
--advanced-camera-card-box-shadow-override,
var(--advanced-camera-card-overlay-message-box-shadow)
var(--advanced-camera-card-notification-box-shadow)
);
pointer-events: auto;
@@ -56,7 +55,7 @@
}
// Exit animation
.message.exiting {
.notification.exiting {
animation: slideDown 0.25s ease-out forwards;
}
@@ -99,12 +98,12 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--advanced-camera-card-overlay-message-icon-color);
color: var(--advanced-camera-card-notification-icon-color);
filter: drop-shadow(
0 0 8px
color-mix(
in oklab,
var(--advanced-camera-card-overlay-message-icon-color),
var(--advanced-camera-card-notification-icon-color),
transparent 60%
)
);
@@ -119,7 +118,7 @@
font-size: 15px;
font-weight: 400;
line-height: 1.5;
color: var(--advanced-camera-card-overlay-message-text-color);
color: var(--advanced-camera-card-notification-text-color);
word-break: break-word;
}
@@ -135,7 +134,7 @@
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
var(--advanced-camera-card-notification-close-color),
transparent 92%
);
}
@@ -150,7 +149,7 @@
color: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
var(--advanced-camera-card-notification-close-color),
transparent 40%
);
@@ -166,17 +165,17 @@
}
// Base control hover color transition (from dimmed to full).
&:not([class^='emphasis-']):hover {
color: var(--advanced-camera-card-overlay-message-close-color);
&:not([class^='severity-']):hover {
color: var(--advanced-camera-card-notification-close-color);
}
&.emphasis-low {
&.severity-low {
color: var(--advanced-camera-card-foreground-severity-low-color);
}
&.emphasis-medium {
&.severity-medium {
color: var(--advanced-camera-card-foreground-severity-medium-color);
}
&.emphasis-high {
&.severity-high {
color: var(--advanced-camera-card-foreground-severity-high-color);
}
}
@@ -194,12 +193,12 @@
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
var(--advanced-camera-card-notification-close-color),
transparent 95%
);
color: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
var(--advanced-camera-card-notification-close-color),
transparent 40%
);
@@ -210,10 +209,10 @@
.close:hover {
background: color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-close-color),
var(--advanced-camera-card-notification-close-color),
transparent 90%
);
color: var(--advanced-camera-card-overlay-message-close-color);
color: var(--advanced-camera-card-notification-close-color);
}
.close advanced-camera-card-icon {
@@ -233,7 +232,7 @@
display: flex;
align-items: center;
gap: 8px;
color: var(--advanced-camera-card-overlay-message-text-color);
color: var(--advanced-camera-card-notification-text-color);
font-size: 14px;
line-height: 1.4;
@@ -261,15 +260,15 @@
}
}
&.emphasis-low advanced-camera-card-icon {
&.severity-low advanced-camera-card-icon {
color: var(--advanced-camera-card-foreground-severity-low-color);
opacity: 1;
}
&.emphasis-medium advanced-camera-card-icon {
&.severity-medium advanced-camera-card-icon {
color: var(--advanced-camera-card-foreground-severity-medium-color);
opacity: 1;
}
&.emphasis-high advanced-camera-card-icon {
&.severity-high advanced-camera-card-icon {
color: var(--advanced-camera-card-foreground-severity-high-color);
opacity: 1;
}
@@ -282,10 +281,10 @@
border-top: 1px solid
color-mix(
in srgb,
var(--advanced-camera-card-overlay-message-text-color),
var(--advanced-camera-card-notification-text-color),
transparent 80%
);
color: var(--advanced-camera-card-overlay-message-text-color);
color: var(--advanced-camera-card-notification-text-color);
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
+1
View File
@@ -64,6 +64,7 @@
.item.action {
cursor: pointer;
pointer-events: auto;
}
img.item,
+12 -12
View File
@@ -286,24 +286,24 @@
transparent 20%
);
/******************
* Overlay Message
******************/
--advanced-camera-card-overlay-message-background: linear-gradient(
/**************
* Notification
**************/
--advanced-camera-card-notification-background: linear-gradient(
135deg,
rgba(35, 35, 35, 0.75) 0%,
rgba(15, 15, 15, 0.65) 100%
);
--advanced-camera-card-overlay-message-backdrop-filter: blur(20px) saturate(180%);
--advanced-camera-card-overlay-message-border: 1px solid rgba(255, 255, 255, 0.1);
--advanced-camera-card-overlay-message-border-top: 1px solid rgba(255, 255, 255, 0.15);
--advanced-camera-card-overlay-message-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5),
--advanced-camera-card-notification-backdrop-filter: blur(20px) saturate(180%);
--advanced-camera-card-notification-border: 1px solid rgba(255, 255, 255, 0.1);
--advanced-camera-card-notification-border-top: 1px solid rgba(255, 255, 255, 0.15);
--advanced-camera-card-notification-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5),
0 2px 8px 0 rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.05);
--advanced-camera-card-overlay-message-text-color: #ffffff;
--advanced-camera-card-overlay-message-icon-color: var(
--advanced-camera-card-notification-text-color: #ffffff;
--advanced-camera-card-notification-icon-color: var(
--advanced-camera-card-foreground-primary
);
--advanced-camera-card-overlay-message-scrollbar-color: rgba(255, 255, 255, 0.2);
--advanced-camera-card-overlay-message-close-color: #ffffff;
--advanced-camera-card-notification-scrollbar-color: rgba(255, 255, 255, 0.2);
--advanced-camera-card-notification-close-color: #ffffff;
}
+10 -10
View File
@@ -522,20 +522,20 @@
// Menu border colors need to re-adjust based on redefined variable.
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
/******************
* Overlay Message
******************/
--advanced-camera-card-overlay-message-background: linear-gradient(
/**************
* Notification
**************/
--advanced-camera-card-notification-background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.85) 0%,
rgba(235, 235, 235, 0.75) 100%
);
--advanced-camera-card-overlay-message-text-color: var(--primary-text-color);
--advanced-camera-card-overlay-message-border: 1px solid rgba(0, 0, 0, 0.3);
--advanced-camera-card-overlay-message-border-top: 1px solid rgba(0, 0, 0, 0.2);
--advanced-camera-card-overlay-message-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3),
--advanced-camera-card-notification-text-color: var(--primary-text-color);
--advanced-camera-card-notification-border: 1px solid rgba(0, 0, 0, 0.3);
--advanced-camera-card-notification-border-top: 1px solid rgba(0, 0, 0, 0.2);
--advanced-camera-card-notification-box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3),
0 2px 8px 0 rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(255, 255, 255, 0.8);
--advanced-camera-card-overlay-message-scrollbar-color: rgba(0, 0, 0, 0.5);
--advanced-camera-card-overlay-message-close-color: #000000;
--advanced-camera-card-notification-scrollbar-color: rgba(0, 0, 0, 0.5);
--advanced-camera-card-notification-close-color: #000000;
--app-header-text-color: white;
}
+1 -1
View File
@@ -15,6 +15,6 @@ $z-index-loading: 6;
// Need menu to render above drawer (so the drawer button is below menu-submenus)
$z-index-menu: 4;
$z-index-overlay-message: 4;
$z-index-notification: 4;
$z-index-drawer: 3;
$z-index-status-bar: 2;
-34
View File
@@ -1,7 +1,6 @@
import { z } from 'zod';
import type { EffectOptions } from './card-controller/effects/types';
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
import { Severity } from './severity';
// UI-facing media types for galleries and views.
export const VIEW_MEDIA_TYPES = ['clips', 'snapshots', 'recordings', 'reviews'] as const;
@@ -64,24 +63,6 @@ export interface Message {
url?: MessageURL;
}
export interface MetadataField {
title: string;
icon?: Icon;
hint?: string;
emphasis?: Severity;
}
export interface OverlayMessageControl extends MetadataField {
callback: () => OverlayMessage | null | Promise<OverlayMessage | null>;
}
export interface OverlayMessage {
heading?: MetadataField;
controls?: OverlayMessageControl[];
details?: MetadataField[];
text?: string;
}
export type WebkitHTMLVideoElement = HTMLVideoElement & {
webkitDisplayingFullscreen: boolean;
webkitSupportsFullscreen: boolean;
@@ -182,21 +163,6 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
'trigger',
] as const;
export interface Icon {
// If set, this icon will be used.
icon?: string;
// If icon is not set, this entity's icon will be used (and HA will be asked
// to render it).
entity?: string;
// Whether or not to change the icon color depending on entity state.
stateColor?: boolean;
// If an icon is not otherwise resolved / available, this will be used instead.
fallback?: string;
}
export interface Interaction {
action: string;
}
+18 -2
View File
@@ -28,10 +28,12 @@ import { SetReviewActionConfig } from '../config/schema/actions/custom/set-revie
import { SubstreamSelectActionConfig } from '../config/schema/actions/custom/substream-select.js';
import { ViewActionConfig } from '../config/schema/actions/custom/view.js';
import { PerformActionActionConfig } from '../config/schema/actions/stock/perform-action.js';
import type { Notification } from '../config/schema/actions/types.js';
import {
ActionConfig,
ActionsConfig,
Actions,
AdvancedCameraCardCustomActionConfig,
NotificationActionConfig,
} from '../config/schema/actions/types.js';
import { AdvancedCameraCardUserSpecifiedView } from '../config/schema/common/const.js';
import { PTZControlType } from '../config/schema/common/controls/ptz.js';
@@ -270,6 +272,20 @@ export function createSetReviewAction(reviewed?: boolean): SetReviewActionConfig
};
}
export function createNotificationAction(
notification: Notification,
options?: {
cardID?: string;
},
): NotificationActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: 'notification',
notification,
...(options?.cardID && { card_id: options.cardID }),
};
}
/**
* Get an action configuration given a config and an interaction (e.g. 'tap').
* @param interaction The interaction: `tap`, `hold` or `double_tap`
@@ -278,7 +294,7 @@ export function createSetReviewAction(reviewed?: boolean): SetReviewActionConfig
*/
export function getActionConfigGivenAction(
interaction?: string,
config?: ActionsConfig | null,
config?: Actions | null,
): ActionConfig | ActionConfig[] | null {
if (!interaction || !config) {
return null;
+5
View File
@@ -0,0 +1,5 @@
import { fireAdvancedCameraCardEvent } from './fire-advanced-camera-card-event.js';
export function dispatchDismissNotificationEvent(element: HTMLElement): void {
fireAdvancedCameraCardEvent(element, 'notification:dismiss');
}
-13
View File
@@ -1,13 +0,0 @@
import { OverlayMessage } from '../types.js';
import { fireAdvancedCameraCardEvent } from './fire-advanced-camera-card-event.js';
export function dispatchShowOverlayMessageEvent(
element: HTMLElement,
message: OverlayMessage,
): void {
fireAdvancedCameraCardEvent(element, 'overlay-message:show', message);
}
export function dispatchDismissOverlayMessageEvent(element: HTMLElement): void {
fireAdvancedCameraCardEvent(element, 'overlay-message:dismiss');
}