feat: Allow user generated notifications (#2401)
This commit is contained in:
@@ -67,3 +67,19 @@
|
|||||||
- **Think through UX before implementing.** For any user-visible change, reason through the full set of states and edge cases it touches. Flag them before writing code, not after.
|
- **Think through UX before implementing.** For any user-visible change, reason through the full set of states and edge cases it touches. Flag them before writing code, not after.
|
||||||
|
|
||||||
- **Be concise.** Short, direct answers are preferred. Skip preamble, avoid restating the question, and don't summarize what you just did unless asked.
|
- **Be concise.** Short, direct answers are preferred. Skip preamble, avoid restating the question, and don't summarize what you just did unless asked.
|
||||||
|
|
||||||
|
- **No re-exports or pass-through files.** Have callers import directly from the source module. Don't create files that only re-export from another file.
|
||||||
|
|
||||||
|
- **Single source of truth for types.** When a Zod schema defines a shape, derive the TypeScript type from it (`z.infer<typeof schema>`). Don't maintain a parallel interface that duplicates the schema.
|
||||||
|
|
||||||
|
- **Separate external API from internal extensions.** User-facing schemas should only contain user-configurable fields. For internal-only fields, create a derived interface (e.g. `interface InternalFoo extends Foo { internalField?: ... }`). Never expose internal plumbing in external schemas.
|
||||||
|
|
||||||
|
- **Extract shared schemas eagerly.** If a schema (e.g. `iconSchema`) could apply to multiple features, put it in `config/schema/common/` from the start rather than nesting it under a specific feature.
|
||||||
|
|
||||||
|
- **Name fields for their semantic purpose.** Choose names that reflect what the field means in context. Ask yourself what the field actually does.
|
||||||
|
|
||||||
|
- **Accurate comment headings.** If a comment section covers multiple related items, the heading must reflect all of them — not just the first one added.
|
||||||
|
|
||||||
|
- **Check CSS inheritance before adding interactive elements.** Parent rules like `pointer-events: none` silently block children. When adding clickable items to a container, verify the full CSS cascade allows interaction.
|
||||||
|
|
||||||
|
- **Preserve alphabetical ordering.** When inserting into a list, file, or set of sections that is fully or mostly alphabetized, maintain that ordering.
|
||||||
|
|||||||
@@ -341,6 +341,52 @@ action: custom:advanced-camera-card-action
|
|||||||
advanced_camera_card_action: mute
|
advanced_camera_card_action: mute
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `notification`
|
||||||
|
|
||||||
|
[](../../common/experimental-warning.md ':include')
|
||||||
|
|
||||||
|
Show a notification overlay on the card with optional controls.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: notification
|
||||||
|
# [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ----------------------------- | --------------------------------------------- |
|
||||||
|
| `action` | Must be `custom:advanced-camera-card-action`. |
|
||||||
|
| `advanced_camera_card_action` | Must be `notification`. |
|
||||||
|
| `notification` | The notification to display. See below. |
|
||||||
|
|
||||||
|
### `notification`
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ---------- | ------------------------------------------------------------------------------------- |
|
||||||
|
| `heading` | An optional heading. Uses the same format as [`details`](README.md?id=details) below. |
|
||||||
|
| `text` | An optional text string to display as the notification body. |
|
||||||
|
| `details` | An optional list of metadata. See [`details`](README.md?id=details) below. |
|
||||||
|
| `controls` | An optional list of controls. See [`controls`](README.md?id=controls) below. |
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
| Parameter | Description |
|
||||||
|
| ---------- | ------------------------------------------------------------- |
|
||||||
|
| `text` | The detail text. |
|
||||||
|
| `icon` | An optional icon (e.g. `mdi:alert`). |
|
||||||
|
| `tooltip` | An optional tooltip string (shown on hover). |
|
||||||
|
| `severity` | An optional severity level, one of `low`, `medium` or `high`. |
|
||||||
|
|
||||||
|
### Controls
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `icon` | | An optional icon (e.g. `mdi:check-circle`). |
|
||||||
|
| `tooltip` | | An optional tooltip string (shown on hover). |
|
||||||
|
| `severity` | | An optional severity level, one of `low`, `medium` or `high`. |
|
||||||
|
| `actions` | | [Actions](../README.md) to execute on interaction. Supports `tap_action`, `hold_action`, `double_tap_action`, `start_tap_action`, `end_tap_action`. See [example](../../../examples.md?id=notifications). |
|
||||||
|
| `dismiss` | `true` | Whether to dismiss the notification after the control's actions execute. |
|
||||||
|
|
||||||
## `pause`
|
## `pause`
|
||||||
|
|
||||||
Pause the selected media.
|
Pause the selected media.
|
||||||
@@ -740,6 +786,29 @@ elements:
|
|||||||
tap_action:
|
tap_action:
|
||||||
action: custom:advanced-camera-card-action
|
action: custom:advanced-camera-card-action
|
||||||
advanced_camera_card_action: info
|
advanced_camera_card_action: info
|
||||||
|
- type: custom:advanced-camera-card-menu-icon
|
||||||
|
icon: mdi:alpha-m-circle-outline
|
||||||
|
title: Show notification
|
||||||
|
tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: notification
|
||||||
|
notification:
|
||||||
|
heading:
|
||||||
|
text: Attention
|
||||||
|
icon: mdi:alert
|
||||||
|
severity: high
|
||||||
|
text: Something important happened.
|
||||||
|
controls:
|
||||||
|
- icon: mdi:filmstrip
|
||||||
|
tooltip: View clips (tap) / View clip (double-tap)
|
||||||
|
actions:
|
||||||
|
tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: clips
|
||||||
|
double_tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: clip
|
||||||
|
dismiss: false
|
||||||
- type: custom:advanced-camera-card-menu-icon
|
- type: custom:advanced-camera-card-menu-icon
|
||||||
icon: mdi:alpha-n-circle
|
icon: mdi:alpha-n-circle
|
||||||
title: Turn off substream
|
title: Turn off substream
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ elements:
|
|||||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||||
| `icon` | | The icon to show in the status bar, e.g. `mdi:camera-front`. See also [custom icons](../../../usage/custom-icons.md). |
|
| `icon` | | The icon to show in the status bar, e.g. `mdi:camera-front`. See also [custom icons](../../../usage/custom-icons.md). |
|
||||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||||
|
| `severity` | | An optional severity level, one of `low`, `medium` or `high`. Colors the item accordingly. |
|
||||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||||
|
|
||||||
## `status-bar-image`
|
## `status-bar-image`
|
||||||
@@ -175,6 +176,7 @@ elements:
|
|||||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||||
| `image` | | The image to show in the status bar, e.g. `https://my.site.com/status.png`. |
|
| `image` | | The image to show in the status bar, e.g. `https://my.site.com/status.png`. |
|
||||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||||
|
| `severity` | | An optional severity level, one of `low`, `medium` or `high`. Colors the item accordingly. |
|
||||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||||
|
|
||||||
## `status-bar-string`
|
## `status-bar-string`
|
||||||
@@ -196,6 +198,7 @@ elements:
|
|||||||
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
| `expand` | `false` | If `false` this status bar item will consume the minimum possible space, if `true` will expand to the available space. |
|
||||||
| `string` | | The string to show in the status bar, e.g. `Intruder detected!` |
|
| `string` | | The string to show in the status bar, e.g. `Intruder detected!` |
|
||||||
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
| `priority` | `50` | The item priority. Higher priority items are ordered closer to the start of the status bar (i.e. an item with priority `70` will order further to the left than an item with priority `60`). Minimum `0`, maximum `100`. |
|
||||||
|
| `severity` | | An optional severity level, one of `low`, `medium` or `high`. Colors the item accordingly. |
|
||||||
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
| `sufficient` | `false` | Whether or not this item is sufficient to display the status bar if it's otherwise hidden (e.g. with the `popup` [status bar style](../../status-bar.md)). |
|
||||||
|
|
||||||
## Fully expanded reference
|
## Fully expanded reference
|
||||||
@@ -331,6 +334,7 @@ elements:
|
|||||||
expand: false
|
expand: false
|
||||||
string: 'Intruder alert!'
|
string: 'Intruder alert!'
|
||||||
priority: 50
|
priority: 50
|
||||||
|
severity: high
|
||||||
sufficient: false
|
sufficient: false
|
||||||
- type: custom:advanced-camera-card-status-bar-icon
|
- type: custom:advanced-camera-card-status-bar-icon
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -338,6 +342,7 @@ elements:
|
|||||||
expand: false
|
expand: false
|
||||||
icon: 'mdi:cow'
|
icon: 'mdi:cow'
|
||||||
priority: 50
|
priority: 50
|
||||||
|
severity: medium
|
||||||
sufficient: false
|
sufficient: false
|
||||||
- type: custom:advanced-camera-card-status-bar-image
|
- type: custom:advanced-camera-card-status-bar-image
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -345,5 +350,6 @@ elements:
|
|||||||
expand: false
|
expand: false
|
||||||
image: https://my.site.com/status.png
|
image: https://my.site.com/status.png
|
||||||
priority: 50
|
priority: 50
|
||||||
|
severity: low
|
||||||
sufficient: false
|
sufficient: false
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ status_bar:
|
|||||||
| `severity` | The media severity indicator (if any) for review severity (e.g. Frigate alerts/detections). |
|
| `severity` | The media severity indicator (if any) for review severity (e.g. Frigate alerts/detections). |
|
||||||
| `technology` | The detected media technology (if any). |
|
| `technology` | The detected media technology (if any). |
|
||||||
| `title` | The media title. |
|
| `title` | The media title. |
|
||||||
|
| `upgrade` | An indicator that appears when a configuration upgrade is available. |
|
||||||
|
|
||||||
### Options for each item
|
### Options for each item
|
||||||
|
|
||||||
@@ -85,4 +86,7 @@ status_bar:
|
|||||||
title:
|
title:
|
||||||
enabled: true
|
enabled: true
|
||||||
priority: 50
|
priority: 50
|
||||||
|
upgrade:
|
||||||
|
enabled: true
|
||||||
|
priority: 50
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -729,6 +729,42 @@ cameras:
|
|||||||
id: office-webrtc
|
id: office-webrtc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
Show a notification with controls that respond to different interactions. In
|
||||||
|
this example, tapping the control navigates to the `clips` (media gallery) view
|
||||||
|
and double-tapping navigates to the `clip` (media player) view.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:advanced-camera-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.office
|
||||||
|
elements:
|
||||||
|
- type: custom:advanced-camera-card-menu-icon
|
||||||
|
icon: mdi:bell
|
||||||
|
title: Show notification
|
||||||
|
tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: notification
|
||||||
|
notification:
|
||||||
|
heading:
|
||||||
|
text: Motion detected
|
||||||
|
icon: mdi:motion-sensor
|
||||||
|
severity: medium
|
||||||
|
text: Motion was detected in the office.
|
||||||
|
controls:
|
||||||
|
- icon: mdi:filmstrip
|
||||||
|
tooltip: View clips (tap) / clip (double-tap)
|
||||||
|
actions:
|
||||||
|
tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: clips
|
||||||
|
double_tap_action:
|
||||||
|
action: custom:advanced-camera-card-action
|
||||||
|
advanced_camera_card_action: clip
|
||||||
|
dismiss: false
|
||||||
|
```
|
||||||
|
|
||||||
## Overriding configuration
|
## Overriding configuration
|
||||||
|
|
||||||
You can override card configuration when certain [conditions](configuration/conditions.md) are met.
|
You can override card configuration when certain [conditions](configuration/conditions.md) are met.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ExpiringEqualityCache } from '../cache/expiring-cache';
|
import { ExpiringEqualityCache } from '../cache/expiring-cache';
|
||||||
import { SSLCiphers } from '../config/schema/cameras';
|
import { SSLCiphers } from '../config/schema/cameras';
|
||||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||||
|
import { InternalIcon } from '../config/schema/common/icon';
|
||||||
import { BaseQuery, QueryFilters, QuerySource } from '../query-source';
|
import { BaseQuery, QueryFilters, QuerySource } from '../query-source';
|
||||||
import { CapabilityKey, Endpoint, Icon } from '../types';
|
import { CapabilityKey, Endpoint } from '../types';
|
||||||
import { ViewMedia } from '../view/item';
|
import { ViewMedia } from '../view/item';
|
||||||
|
|
||||||
// ====
|
// ====
|
||||||
@@ -118,7 +119,7 @@ export interface CapabilitySearchOptions {
|
|||||||
|
|
||||||
export interface CameraManagerCameraMetadata {
|
export interface CameraManagerCameraMetadata {
|
||||||
title: string;
|
title: string;
|
||||||
icon: Icon;
|
icon: InternalIcon;
|
||||||
|
|
||||||
// Engine icon is just a string since it will never be entity-derived.
|
// Engine icon is just a string since it will never be entity-derived.
|
||||||
engineIcon?: string;
|
engineIcon?: string;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export class InfoAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(api.getCameraManager(), item);
|
controller.calculate(api.getCameraManager(), item);
|
||||||
|
|
||||||
api.getOverlayMessageManager().setMessage(
|
api.getNotificationManager().setNotification(
|
||||||
controller.getMessage({
|
controller.getNotification({
|
||||||
hass: api.getHASSManager().getHASS() ?? undefined,
|
hass: api.getHASSManager().getHASS() ?? undefined,
|
||||||
viewItemManager: api.getViewItemManager(),
|
viewItemManager: api.getViewItemManager(),
|
||||||
viewManagerEpoch: api.getViewManager().getEpoch(),
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import { MoreInfoAction } from './actions/more-info';
|
|||||||
import { MuteAction } from './actions/mute';
|
import { MuteAction } from './actions/mute';
|
||||||
import { NavigateAction } from './actions/navigate';
|
import { NavigateAction } from './actions/navigate';
|
||||||
import { NoneAction } from './actions/none';
|
import { NoneAction } from './actions/none';
|
||||||
|
import { NotificationAction } from './actions/notification';
|
||||||
import { PauseAction } from './actions/pause';
|
import { PauseAction } from './actions/pause';
|
||||||
import { PerformActionAction } from './actions/perform-action';
|
import { PerformActionAction } from './actions/perform-action';
|
||||||
import { PIPAction } from './actions/pip';
|
import { PIPAction } from './actions/pip';
|
||||||
@@ -163,6 +164,8 @@ export class ActionFactory {
|
|||||||
return new PTZControlsAction(context, action, options?.config);
|
return new PTZControlsAction(context, action, options?.config);
|
||||||
case 'log':
|
case 'log':
|
||||||
return new LogAction(context, action, options?.config);
|
return new LogAction(context, action, options?.config);
|
||||||
|
case 'notification':
|
||||||
|
return new NotificationAction(context, action, options?.config);
|
||||||
case 'status_bar':
|
case 'status_bar':
|
||||||
return new StatusBarAction(context, action, options?.config);
|
return new StatusBarAction(context, action, options?.config);
|
||||||
case 'reload':
|
case 'reload':
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class ConfigManager {
|
|||||||
private _overriddenConfig: AdvancedCameraCardConfig | null = null;
|
private _overriddenConfig: AdvancedCameraCardConfig | null = null;
|
||||||
private _rawConfig: RawAdvancedCameraCardConfig | null = null;
|
private _rawConfig: RawAdvancedCameraCardConfig | null = null;
|
||||||
private _cardWideConfig: CardWideConfig | null = null;
|
private _cardWideConfig: CardWideConfig | null = null;
|
||||||
|
private _upgradeable = false;
|
||||||
private _overridesManager = new OverridesManager(() => this._processOverrideConfig());
|
private _overridesManager = new OverridesManager(() => this._processOverrideConfig());
|
||||||
|
|
||||||
constructor(api: CardConfigAPI) {
|
constructor(api: CardConfigAPI) {
|
||||||
@@ -54,17 +55,22 @@ export class ConfigManager {
|
|||||||
return this._rawConfig;
|
return this._rawConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isUpgradeable(): boolean {
|
||||||
|
return this._upgradeable;
|
||||||
|
}
|
||||||
|
|
||||||
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
|
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
|
||||||
if (!inputConfig) {
|
if (!inputConfig) {
|
||||||
throw new Error(localize('error.invalid_configuration'));
|
throw new Error(localize('error.invalid_configuration'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
|
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
|
||||||
|
this._upgradeable = isConfigUpgradeable(inputConfig);
|
||||||
|
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
|
||||||
const hint = getParseError(parseResult.error);
|
const hint = getParseError(parseResult.error);
|
||||||
let upgradeMessage = '';
|
let upgradeMessage = '';
|
||||||
if (configUpgradeable) {
|
if (this._upgradeable) {
|
||||||
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { MediaLoadedInfoManager } from './media-info-manager';
|
|||||||
import { MediaPlayerManager } from './media-player-manager';
|
import { MediaPlayerManager } from './media-player-manager';
|
||||||
import { MessageManager } from './message-manager';
|
import { MessageManager } from './message-manager';
|
||||||
import { MicrophoneManager } from './microphone-manager';
|
import { MicrophoneManager } from './microphone-manager';
|
||||||
import { OverlayMessageManager } from './overlay-message-manager';
|
import { NotificationManager } from './notification-manager';
|
||||||
import { PIPManager } from './pip-manager';
|
import { PIPManager } from './pip-manager';
|
||||||
import { QueryStringManager } from './query-string-manager';
|
import { QueryStringManager } from './query-string-manager';
|
||||||
import { StatusBarItemManager } from './status-bar-item-manager';
|
import { StatusBarItemManager } from './status-bar-item-manager';
|
||||||
@@ -58,7 +58,7 @@ import {
|
|||||||
CardMediaPlayerAPI,
|
CardMediaPlayerAPI,
|
||||||
CardMessageAPI,
|
CardMessageAPI,
|
||||||
CardMicrophoneAPI,
|
CardMicrophoneAPI,
|
||||||
CardOverlayMessageAPI,
|
CardNotificationAPI,
|
||||||
CardPIPAPI,
|
CardPIPAPI,
|
||||||
CardQueryStringAPI,
|
CardQueryStringAPI,
|
||||||
CardStyleAPI,
|
CardStyleAPI,
|
||||||
@@ -90,7 +90,7 @@ export class CardController
|
|||||||
CardMediaPlayerAPI,
|
CardMediaPlayerAPI,
|
||||||
CardMessageAPI,
|
CardMessageAPI,
|
||||||
CardMicrophoneAPI,
|
CardMicrophoneAPI,
|
||||||
CardOverlayMessageAPI,
|
CardNotificationAPI,
|
||||||
CardQueryStringAPI,
|
CardQueryStringAPI,
|
||||||
CardStyleAPI,
|
CardStyleAPI,
|
||||||
CardTriggersAPI,
|
CardTriggersAPI,
|
||||||
@@ -125,7 +125,7 @@ export class CardController
|
|||||||
private _mediaPlayerManager = new MediaPlayerManager(this);
|
private _mediaPlayerManager = new MediaPlayerManager(this);
|
||||||
private _messageManager = new MessageManager(this);
|
private _messageManager = new MessageManager(this);
|
||||||
private _microphoneManager = new MicrophoneManager(this);
|
private _microphoneManager = new MicrophoneManager(this);
|
||||||
private _overlayMessageManager = new OverlayMessageManager(this);
|
private _notificationManager = new NotificationManager(this);
|
||||||
private _pipManager = new PIPManager(this);
|
private _pipManager = new PIPManager(this);
|
||||||
private _queryStringManager = new QueryStringManager(this);
|
private _queryStringManager = new QueryStringManager(this);
|
||||||
private _statusBarItemManager = new StatusBarItemManager(this);
|
private _statusBarItemManager = new StatusBarItemManager(this);
|
||||||
@@ -252,8 +252,8 @@ export class CardController
|
|||||||
this._microphoneManager = new MicrophoneManager(this);
|
this._microphoneManager = new MicrophoneManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOverlayMessageManager(): OverlayMessageManager {
|
public getNotificationManager(): NotificationManager {
|
||||||
return this._overlayMessageManager;
|
return this._notificationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPIPManager(): PIPManager {
|
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 { CameraManager } from '../camera-manager/manager';
|
||||||
import { StatusBarItem } from '../config/schema/actions/types';
|
import { StatusBarItem } from '../config/schema/actions/types';
|
||||||
import { StatusBarConfig } from '../config/schema/status-bar';
|
import { StatusBarConfig } from '../config/schema/status-bar';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
import { MediaLoadedInfo } from '../types';
|
import { MediaLoadedInfo } from '../types';
|
||||||
|
import { createNotificationAction } from '../utils/action';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { CardStatusBarAPI } from './types';
|
import { CardStatusBarAPI } from './types';
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ export class StatusBarItemManager {
|
|||||||
cameraManager?: CameraManager | null;
|
cameraManager?: CameraManager | null;
|
||||||
view?: View | null;
|
view?: View | null;
|
||||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
|
isUpgradeable?: boolean;
|
||||||
}): StatusBarItem[] {
|
}): StatusBarItem[] {
|
||||||
const cameraMetadata = options?.view?.camera
|
const cameraMetadata = options?.view?.camera
|
||||||
? options?.cameraManager?.getCameraMetadata(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,
|
...this._dynamicItems,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type { MediaLoadedInfoManager } from './media-info-manager';
|
|||||||
import type { MediaPlayerManager } from './media-player-manager';
|
import type { MediaPlayerManager } from './media-player-manager';
|
||||||
import type { MessageManager } from './message-manager';
|
import type { MessageManager } from './message-manager';
|
||||||
import type { MicrophoneManager } from './microphone-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 { PIPManager } from './pip-manager';
|
||||||
import type { QueryStringManager } from './query-string-manager';
|
import type { QueryStringManager } from './query-string-manager';
|
||||||
import type { StatusBarItemManager } from './status-bar-item-manager';
|
import type { StatusBarItemManager } from './status-bar-item-manager';
|
||||||
@@ -54,7 +54,7 @@ export interface CardActionsAPI {
|
|||||||
getMediaPlayerManager(): MediaPlayerManager;
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
getMicrophoneManager(): MicrophoneManager;
|
getMicrophoneManager(): MicrophoneManager;
|
||||||
getOverlayMessageManager(): OverlayMessageManager;
|
getNotificationManager(): NotificationManager;
|
||||||
getPIPManager(): PIPManager;
|
getPIPManager(): PIPManager;
|
||||||
getStatusBarItemManager(): StatusBarItemManager;
|
getStatusBarItemManager(): StatusBarItemManager;
|
||||||
getTriggersManager(): TriggersManager;
|
getTriggersManager(): TriggersManager;
|
||||||
@@ -257,7 +257,7 @@ export interface CardMessageAPI {
|
|||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardOverlayMessageAPI {
|
export interface CardNotificationAPI {
|
||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -16,7 +16,7 @@ import './components/menu.js';
|
|||||||
import { AdvancedCameraCardMenu } from './components/menu.js';
|
import { AdvancedCameraCardMenu } from './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import { renderMessage } from './components/message.js';
|
import { renderMessage } from './components/message.js';
|
||||||
import './components/overlay-message.js';
|
import './components/notification.js';
|
||||||
import './components/overlay.js';
|
import './components/overlay.js';
|
||||||
import { AdvancedCameraCardOverlay } from './components/overlay.js';
|
import { AdvancedCameraCardOverlay } from './components/overlay.js';
|
||||||
import './components/status-bar';
|
import './components/status-bar';
|
||||||
@@ -32,7 +32,7 @@ import { REPO_URL } from './const.js';
|
|||||||
import { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
|
import { HomeAssistant, LovelaceCardEditor } from './ha/types.js';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import cardStyle from './scss/card.scss';
|
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 { hasAction } from './utils/action.js';
|
||||||
import { getReleaseVersion } from './utils/diagnostics';
|
import { getReleaseVersion } from './utils/diagnostics';
|
||||||
|
|
||||||
@@ -288,6 +288,7 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
cameraManager: this._controller.getCameraManager(),
|
cameraManager: this._controller.getCameraManager(),
|
||||||
view: this._controller.getViewManager().getView(),
|
view: this._controller.getViewManager().getView(),
|
||||||
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
mediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
||||||
|
isUpgradeable: this._controller.getConfigManager().isUpgradeable(),
|
||||||
})}
|
})}
|
||||||
.config=${this._config.status_bar}
|
.config=${this._config.status_bar}
|
||||||
></advanced-camera-card-status-bar>
|
></advanced-camera-card-status-bar>
|
||||||
@@ -374,11 +375,8 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
() => this.requestUpdate() /* Refresh play/pause menu button */
|
() => this.requestUpdate() /* Refresh play/pause menu button */
|
||||||
}
|
}
|
||||||
@advanced-camera-card:focus=${() => this.focus()}
|
@advanced-camera-card:focus=${() => this.focus()}
|
||||||
@advanced-camera-card:overlay-message:show=${(
|
@advanced-camera-card:notification:dismiss=${() =>
|
||||||
ev: CustomEvent<OverlayMessage>,
|
this._controller.getNotificationManager().reset()}
|
||||||
) => this._controller.getOverlayMessageManager().setMessage(ev.detail)}
|
|
||||||
@advanced-camera-card:overlay-message:dismiss=${() =>
|
|
||||||
this._controller.getOverlayMessageManager().reset()}
|
|
||||||
>
|
>
|
||||||
${showLoading
|
${showLoading
|
||||||
? html`<advanced-camera-card-loading
|
? html`<advanced-camera-card-loading
|
||||||
@@ -459,10 +457,12 @@ class AdvancedCameraCard extends LitElement {
|
|||||||
>
|
>
|
||||||
</advanced-camera-card-elements>`
|
</advanced-camera-card-elements>`
|
||||||
: ``}
|
: ``}
|
||||||
${this._controller.getOverlayMessageManager().getMessage()
|
${this._controller.getNotificationManager().getNotification()
|
||||||
? html`<advanced-camera-card-overlay-message
|
? html`<advanced-camera-card-notification
|
||||||
.message=${this._controller.getOverlayMessageManager().getMessage()}
|
.notification=${this._controller
|
||||||
></advanced-camera-card-overlay-message>`
|
.getNotificationManager()
|
||||||
|
.getNotification()}
|
||||||
|
></advanced-camera-card-notification>`
|
||||||
: ''}
|
: ''}
|
||||||
</ha-card>`,
|
</ha-card>`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import frigateSVG from '../camera-manager/frigate/assets/frigate.svg';
|
|||||||
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
|
import motioneyeSVG from '../camera-manager/motioneye/assets/motioneye.svg';
|
||||||
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
|
import reolinkSVG from '../camera-manager/reolink/assets/reolink.svg';
|
||||||
import tplinkSVG from '../camera-manager/tplink/assets/tplink.svg';
|
import tplinkSVG from '../camera-manager/tplink/assets/tplink.svg';
|
||||||
|
import { Icon } from '../config/schema/common/icon';
|
||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
import irisSVG from '../images/iris.svg';
|
import irisSVG from '../images/iris.svg';
|
||||||
import { Icon } from '../types';
|
|
||||||
|
|
||||||
export class IconController {
|
export class IconController {
|
||||||
public getCustomIcon(icon?: Icon): string | null {
|
public getCustomIcon(icon?: Icon): string | null {
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import { CameraManager } from '../../camera-manager/manager';
|
|||||||
import { CameraManagerCameraMetadata } from '../../camera-manager/types';
|
import { CameraManagerCameraMetadata } from '../../camera-manager/types';
|
||||||
import { ViewItemManager } from '../../card-controller/view/item-manager';
|
import { ViewItemManager } from '../../card-controller/view/item-manager';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types';
|
import { ViewManagerEpoch } from '../../card-controller/view/types';
|
||||||
|
import {
|
||||||
|
Notification,
|
||||||
|
NotificationControl,
|
||||||
|
NotificationDetail,
|
||||||
|
} from '../../config/schema/actions/types';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../../types';
|
import { createInternalCallbackAction } from '../../utils/action';
|
||||||
import { getDurationString, prettifyTitle } from '../../utils/basic';
|
import { getDurationString, prettifyTitle } from '../../utils/basic';
|
||||||
import {
|
import {
|
||||||
downloadMedia,
|
downloadMedia,
|
||||||
@@ -17,7 +22,7 @@ import { ViewItem } from '../../view/item';
|
|||||||
import { ViewItemClassifier } from '../../view/item-classifier';
|
import { ViewItemClassifier } from '../../view/item-classifier';
|
||||||
import { ViewItemCapabilities } from '../../view/types';
|
import { ViewItemCapabilities } from '../../view/types';
|
||||||
|
|
||||||
export interface OverlayControlsContext {
|
export interface NotificationControlsContext {
|
||||||
hass?: HomeAssistant;
|
hass?: HomeAssistant;
|
||||||
viewItemManager?: ViewItemManager;
|
viewItemManager?: ViewItemManager;
|
||||||
viewManagerEpoch?: ViewManagerEpoch;
|
viewManagerEpoch?: ViewManagerEpoch;
|
||||||
@@ -29,8 +34,8 @@ export interface OverlayControlsContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MediaDetailsController {
|
export class MediaDetailsController {
|
||||||
private _details: MetadataField[] = [];
|
private _details: NotificationDetail[] = [];
|
||||||
private _heading: MetadataField | null = null;
|
private _heading: NotificationDetail | null = null;
|
||||||
private _item: ViewItem | null = null;
|
private _item: ViewItem | null = null;
|
||||||
|
|
||||||
public calculate(
|
public calculate(
|
||||||
@@ -61,7 +66,7 @@ export class MediaDetailsController {
|
|||||||
const score = rawScore ? (rawScore * 100).toFixed(2) + '%' : null;
|
const score = rawScore ? (rawScore * 100).toFixed(2) + '%' : null;
|
||||||
|
|
||||||
this._heading = whatWithTags
|
this._heading = whatWithTags
|
||||||
? { title: `${whatWithTags}${score ? ` ${score}` : ''}` }
|
? { text: `${whatWithTags}${score ? ` ${score}` : ''}` }
|
||||||
: null;
|
: null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,13 +77,13 @@ export class MediaDetailsController {
|
|||||||
|
|
||||||
this._heading = title
|
this._heading = title
|
||||||
? {
|
? {
|
||||||
title: title,
|
text: title,
|
||||||
emphasis: severity ?? undefined,
|
severity: severity ?? undefined,
|
||||||
hint:
|
tooltip:
|
||||||
localize('common.severity') +
|
localize('common.severity') +
|
||||||
': ' +
|
': ' +
|
||||||
localize('common.severities.' + severity),
|
localize('common.severities.' + severity),
|
||||||
icon: { icon: 'mdi:circle-medium' },
|
icon: 'mdi:circle-medium',
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
return;
|
return;
|
||||||
@@ -86,7 +91,7 @@ export class MediaDetailsController {
|
|||||||
|
|
||||||
if (cameraMetadata?.title) {
|
if (cameraMetadata?.title) {
|
||||||
this._heading = {
|
this._heading = {
|
||||||
title: cameraMetadata.title,
|
text: cameraMetadata.title,
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,54 +126,54 @@ export class MediaDetailsController {
|
|||||||
...(startTime
|
...(startTime
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.start'),
|
tooltip: localize('thumbnail.start'),
|
||||||
icon: { icon: 'mdi:calendar-clock-outline' },
|
icon: 'mdi:calendar-clock-outline',
|
||||||
title: format(startTime, 'yyyy-MM-dd HH:mm:ss'),
|
text: format(startTime, 'yyyy-MM-dd HH:mm:ss'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(duration || inProgress
|
...(duration || inProgress
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.duration'),
|
tooltip: localize('thumbnail.duration'),
|
||||||
icon: { icon: 'mdi:clock-outline' },
|
icon: 'mdi:clock-outline',
|
||||||
title: `${duration ?? ''}${duration && inProgress ? ' ' : ''}${inProgress ?? ''}`,
|
text: `${duration ?? ''}${duration && inProgress ? ' ' : ''}${inProgress ?? ''}`,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(cameraMetadata?.title
|
...(cameraMetadata?.title
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.camera'),
|
tooltip: localize('thumbnail.camera'),
|
||||||
title: cameraMetadata.title,
|
text: cameraMetadata.title,
|
||||||
icon: { icon: 'mdi:cctv' },
|
icon: 'mdi:cctv',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(where
|
...(where
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.where'),
|
tooltip: localize('thumbnail.where'),
|
||||||
title: where,
|
text: where,
|
||||||
icon: { icon: 'mdi:map-marker-outline' },
|
icon: 'mdi:map-marker-outline',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(tags
|
...(tags
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.tag'),
|
tooltip: localize('thumbnail.tag'),
|
||||||
title: tags,
|
text: tags,
|
||||||
icon: { icon: 'mdi:tag' },
|
icon: 'mdi:tag',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(seekString
|
...(seekString
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
hint: localize('thumbnail.seek'),
|
tooltip: localize('thumbnail.seek'),
|
||||||
title: seekString,
|
text: seekString,
|
||||||
icon: { icon: 'mdi:clock-fast' },
|
icon: 'mdi:clock-fast',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -183,10 +188,10 @@ export class MediaDetailsController {
|
|||||||
...(includeTitle && itemTitle
|
...(includeTitle && itemTitle
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: itemTitle,
|
text: itemTitle,
|
||||||
...(details.length > 0 && {
|
...(details.length > 0 && {
|
||||||
icon: { icon: 'mdi:rename' },
|
icon: 'mdi:rename',
|
||||||
hint: localize('thumbnail.title'),
|
tooltip: localize('thumbnail.title'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -195,20 +200,15 @@ export class MediaDetailsController {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getHeading(): MetadataField | null {
|
public getHeading(): NotificationDetail | null {
|
||||||
return this._heading;
|
return this._heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDetails(): MetadataField[] {
|
public getDetails(): NotificationDetail[] {
|
||||||
return this._details;
|
return this._details;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public getNotification(context?: NotificationControlsContext): Notification {
|
||||||
* Get an overlay message for the item.
|
|
||||||
* @param context Optional context to include controls.
|
|
||||||
* @returns An OverlayMessage.
|
|
||||||
*/
|
|
||||||
public getMessage(context?: OverlayControlsContext): OverlayMessage {
|
|
||||||
return {
|
return {
|
||||||
heading: this._heading ?? undefined,
|
heading: this._heading ?? undefined,
|
||||||
controls: context ? this._getControls(context) : undefined,
|
controls: context ? this._getControls(context) : undefined,
|
||||||
@@ -219,8 +219,8 @@ export class MediaDetailsController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getControls(context: OverlayControlsContext): OverlayMessageControl[] {
|
private _getControls(context: NotificationControlsContext): NotificationControl[] {
|
||||||
const controls: OverlayMessageControl[] = [];
|
const controls: NotificationControl[] = [];
|
||||||
const item = this._item;
|
const item = this._item;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@@ -230,57 +230,71 @@ export class MediaDetailsController {
|
|||||||
if (ViewItemClassifier.isReview(item)) {
|
if (ViewItemClassifier.isReview(item)) {
|
||||||
const isReviewed = item.isReviewed();
|
const isReviewed = item.isReviewed();
|
||||||
controls.push({
|
controls.push({
|
||||||
title: isReviewed
|
tooltip: isReviewed
|
||||||
? localize('common.set_reviews.unreviewed')
|
? localize('common.set_reviews.unreviewed')
|
||||||
: localize('common.set_reviews.reviewed'),
|
: localize('common.set_reviews.reviewed'),
|
||||||
icon: { icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline' },
|
icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline',
|
||||||
callback: async () => {
|
actions: {
|
||||||
|
tap_action: createInternalCallbackAction(async (api) => {
|
||||||
const success = await toggleReviewed(
|
const success = await toggleReviewed(
|
||||||
item,
|
item,
|
||||||
context.viewItemManager,
|
context.viewItemManager,
|
||||||
context.viewManagerEpoch,
|
context.viewManagerEpoch,
|
||||||
context.filterReviewed,
|
context.filterReviewed,
|
||||||
);
|
);
|
||||||
return success ? this.getMessage(context) : null;
|
if (success) {
|
||||||
|
api
|
||||||
|
.getNotificationManager()
|
||||||
|
.setNotification(this.getNotification(context));
|
||||||
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
dismiss: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.capabilities?.canFavorite && ViewItemClassifier.isMedia(item)) {
|
if (context.capabilities?.canFavorite && ViewItemClassifier.isMedia(item)) {
|
||||||
const isFavorite = item.isFavorite();
|
const isFavorite = item.isFavorite();
|
||||||
controls.push({
|
controls.push({
|
||||||
title: localize('thumbnail.retain_indefinitely'),
|
tooltip: localize('thumbnail.retain_indefinitely'),
|
||||||
icon: { icon: isFavorite ? 'mdi:star' : 'mdi:star-outline' },
|
icon: isFavorite ? 'mdi:star' : 'mdi:star-outline',
|
||||||
emphasis: isFavorite ? 'medium' : undefined,
|
severity: isFavorite ? 'medium' : undefined,
|
||||||
callback: async () => {
|
actions: {
|
||||||
|
tap_action: createInternalCallbackAction(async (api) => {
|
||||||
const success = await toggleFavorite(item, context.viewItemManager);
|
const success = await toggleFavorite(item, context.viewItemManager);
|
||||||
return success ? this.getMessage(context) : null;
|
if (success) {
|
||||||
|
api
|
||||||
|
.getNotificationManager()
|
||||||
|
.setNotification(this.getNotification(context));
|
||||||
|
}
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
dismiss: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.capabilities?.canDownload && item.getID()) {
|
if (context.capabilities?.canDownload && item.getID()) {
|
||||||
controls.push({
|
controls.push({
|
||||||
title: localize('thumbnail.download'),
|
tooltip: localize('thumbnail.download'),
|
||||||
icon: { icon: 'mdi:download' },
|
icon: 'mdi:download',
|
||||||
callback: async () => {
|
dismiss: true,
|
||||||
|
actions: {
|
||||||
|
tap_action: createInternalCallbackAction(async () => {
|
||||||
await downloadMedia(item, context.viewItemManager);
|
await downloadMedia(item, context.viewItemManager);
|
||||||
|
}),
|
||||||
// Close overlay message after download.
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ViewItemClassifier.supportsTimeline(item) && context.viewManagerEpoch) {
|
if (ViewItemClassifier.supportsTimeline(item) && context.viewManagerEpoch) {
|
||||||
controls.push({
|
controls.push({
|
||||||
title: localize('thumbnail.timeline'),
|
tooltip: localize('thumbnail.timeline'),
|
||||||
icon: { icon: 'mdi:target' },
|
icon: 'mdi:target',
|
||||||
callback: () => {
|
dismiss: true,
|
||||||
|
actions: {
|
||||||
|
tap_action: createInternalCallbackAction(async () => {
|
||||||
navigateToTimeline(item, context.viewManagerEpoch);
|
navigateToTimeline(item, context.viewManagerEpoch);
|
||||||
|
}),
|
||||||
// Close overlay after timeline navigation
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { IconController } from '../components-lib/icon-controller';
|
import { IconController } from '../components-lib/icon-controller';
|
||||||
|
import { InternalIcon } from '../config/schema/common/icon.js';
|
||||||
import { HomeAssistant } from '../ha/types';
|
import { HomeAssistant } from '../ha/types';
|
||||||
import iconStyle from '../scss/icon.scss';
|
import iconStyle from '../scss/icon.scss';
|
||||||
import { Icon } from '../types.js';
|
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-icon')
|
@customElement('advanced-camera-card-icon')
|
||||||
@@ -19,7 +19,7 @@ export class AdvancedCameraCardIcon extends LitElement {
|
|||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public icon?: Icon;
|
public icon?: InternalIcon;
|
||||||
|
|
||||||
// Note: This attribute will allow non-active entity state styles (e.g. 'off',
|
// 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
|
// 'unavailable') to be overriden from outside the icon itself. This is useful
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit
|
|||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { NextPreviousControlConfig } from '../config/schema/common/controls/next-previous.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 { HomeAssistant } from '../ha/types.js';
|
||||||
import controlStyle from '../scss/next-previous-control.scss';
|
import controlStyle from '../scss/next-previous-control.scss';
|
||||||
import { Icon } from '../types.js';
|
|
||||||
import { renderTask } from '../utils/task.js';
|
import { renderTask } from '../utils/task.js';
|
||||||
import { createFetchThumbnailTask } from '../utils/thumbnail.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 { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import overlayMessageStyle from '../scss/overlay-message.scss';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../types.js';
|
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request';
|
||||||
import { dispatchDismissOverlayMessageEvent } from '../utils/overlay-message.js';
|
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';
|
import './icon.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-overlay-message')
|
@customElement('advanced-camera-card-notification')
|
||||||
export class AdvancedCameraCardOverlayMessage extends LitElement {
|
export class AdvancedCameraCardNotification extends LitElement {
|
||||||
@property({ attribute: false })
|
@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 {
|
public connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
@@ -29,20 +41,20 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.message) {
|
if (!this.notification) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const heading = this.message.heading;
|
const heading = this.notification.heading;
|
||||||
const details = this.message.details ?? [];
|
const details = this.notification.details ?? [];
|
||||||
const text = this.message.text;
|
const text = this.notification.text;
|
||||||
const controls = this.message.controls ?? [];
|
const controls = this.notification.controls ?? [];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="backdrop" @click=${this._dismiss}></div>
|
<div class="backdrop" @click=${this._dismiss}></div>
|
||||||
<div
|
<div
|
||||||
class="message"
|
class="notification"
|
||||||
${ref(this._refMessage)}
|
${ref(this._refNotification)}
|
||||||
@animationend=${this._handleAnimationEnd}
|
@animationend=${this._handleAnimationEnd}
|
||||||
>
|
>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
@@ -64,60 +76,70 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderControl(control: OverlayMessageControl): TemplateResult {
|
private _renderControl(control: NotificationControl): TemplateResult {
|
||||||
const emphasisClass = control.emphasis ? `emphasis-${control.emphasis}` : '';
|
const severityClass = control.severity ? `severity-${control.severity}` : '';
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="control ${emphasisClass}"
|
class="control ${severityClass}"
|
||||||
title=${control.title}
|
title=${control.tooltip ?? ''}
|
||||||
@click=${async () => this._handleControlClick(control)}
|
.actionHandler=${actionHandler({
|
||||||
|
hasHold: hasAction(control.actions?.hold_action),
|
||||||
|
hasDoubleClick: hasAction(control.actions?.double_tap_action),
|
||||||
|
})}
|
||||||
|
@action=${(ev: CustomEvent) => this._handleControlAction(ev, control)}
|
||||||
>
|
>
|
||||||
${control.icon
|
${control.icon
|
||||||
? html`<advanced-camera-card-icon
|
? html`<advanced-camera-card-icon
|
||||||
.icon=${control.icon}
|
.icon=${{ icon: control.icon }}
|
||||||
></advanced-camera-card-icon>`
|
></advanced-camera-card-icon>`
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _handleControlClick(control: OverlayMessageControl): Promise<void> {
|
private _handleControlAction(
|
||||||
const result = await control.callback();
|
ev: CustomEvent<{ action: string }>,
|
||||||
if (result === null) {
|
control: NotificationControl,
|
||||||
// null = close the message
|
): void {
|
||||||
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
|
|
||||||
|
const action = getActionConfigGivenAction(ev.detail.action, control.actions);
|
||||||
|
if (action) {
|
||||||
|
dispatchActionExecutionRequest(this, {
|
||||||
|
actions: arrayify(action),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (control.dismiss !== false) {
|
||||||
this._dismiss();
|
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 = {
|
const classes = {
|
||||||
detail: true,
|
detail: true,
|
||||||
heading: isHeading,
|
heading: isHeading,
|
||||||
[`emphasis-${detail.emphasis}`]: !!detail.emphasis,
|
[`severity-${detail.severity}`]: !!detail.severity,
|
||||||
};
|
};
|
||||||
return html`
|
return html`
|
||||||
<div class="${classMap(classes)}">
|
<div class="${classMap(classes)}">
|
||||||
${detail.icon
|
${detail.icon
|
||||||
? html`<advanced-camera-card-icon
|
? html`<advanced-camera-card-icon
|
||||||
title=${detail.hint ?? ''}
|
title=${detail.tooltip ?? ''}
|
||||||
.icon=${detail.icon}
|
.icon=${{ icon: detail.icon }}
|
||||||
></advanced-camera-card-icon>`
|
></advanced-camera-card-icon>`
|
||||||
: ''}
|
: ''}
|
||||||
<span title=${detail.title}>${detail.title}</span>
|
<span title=${detail.text}>${detail.text}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dismiss = (): void => {
|
private _dismiss = (): void => {
|
||||||
this._refMessage.value?.classList.add('exiting');
|
this._refNotification.value?.classList.add('exiting');
|
||||||
};
|
};
|
||||||
|
|
||||||
private _handleAnimationEnd = (ev: AnimationEvent): void => {
|
private _handleAnimationEnd = (ev: AnimationEvent): void => {
|
||||||
if (ev.animationName === 'slideDown') {
|
if (ev.animationName === 'slideDown') {
|
||||||
dispatchDismissOverlayMessageEvent(this);
|
dispatchDismissNotificationEvent(this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,12 +158,12 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(overlayMessageStyle);
|
return unsafeCSS(notificationStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'advanced-camera-card-overlay-message': AdvancedCameraCardOverlayMessage;
|
'advanced-camera-card-notification': AdvancedCameraCardNotification;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.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 { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
|
||||||
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
|
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
|
||||||
import { computeDomain } from '../../ha/compute-domain.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 { EntityRegistryManager } from '../../ha/registry/entity/types.js';
|
||||||
import { HomeAssistant } from '../../ha/types.js';
|
import { HomeAssistant } from '../../ha/types.js';
|
||||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||||
import { Icon } from '../../types.js';
|
|
||||||
import { createSelectOptionAction } from '../../utils/action.js';
|
import { createSelectOptionAction } from '../../utils/action.js';
|
||||||
import '../icon.js';
|
import '../icon.js';
|
||||||
import './index.js';
|
import './index.js';
|
||||||
@@ -37,7 +37,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
private _optionTitles?: Record<string, string>;
|
private _optionTitles?: Record<string, string>;
|
||||||
|
|
||||||
private _generatedSubmenuItems?: MenuSubmenuItem[];
|
private _generatedSubmenuItems?: MenuSubmenuItem[];
|
||||||
private _generatedIcon?: Icon;
|
private _generatedIcon?: InternalIcon;
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
// No need to update the submenu unless the select entity has changed.
|
// No need to update the submenu unless the select entity has changed.
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager';
|
import { CameraManager } from '../../camera-manager/manager';
|
||||||
import { MediaDetailsController } from '../../components-lib/media/details-controller';
|
import { MediaDetailsController } from '../../components-lib/media/details-controller';
|
||||||
|
import { NotificationDetail } from '../../config/schema/actions/types';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import thumbnailDetailsStyle from '../../scss/thumbnail-details.scss';
|
import thumbnailDetailsStyle from '../../scss/thumbnail-details.scss';
|
||||||
import { MetadataField } from '../../types';
|
|
||||||
import { ViewItem } from '../../view/item';
|
import { ViewItem } from '../../view/item';
|
||||||
import '../icon';
|
import '../icon';
|
||||||
|
|
||||||
@@ -42,7 +43,10 @@ export class AdvancedCameraCardThumbnailDetails extends LitElement {
|
|||||||
const heading = this._controller.getHeading();
|
const heading = this._controller.getHeading();
|
||||||
const details = this._controller.getDetails();
|
const details = this._controller.getDetails();
|
||||||
|
|
||||||
const renderDetail = (detail: MetadataField, heading = false): TemplateResult => {
|
const renderDetail = (
|
||||||
|
detail: NotificationDetail,
|
||||||
|
heading = false,
|
||||||
|
): TemplateResult => {
|
||||||
return html`<div
|
return html`<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
heading,
|
heading,
|
||||||
@@ -50,12 +54,12 @@ export class AdvancedCameraCardThumbnailDetails extends LitElement {
|
|||||||
>
|
>
|
||||||
${detail.icon
|
${detail.icon
|
||||||
? html` <advanced-camera-card-icon
|
? html` <advanced-camera-card-icon
|
||||||
severity=${detail.emphasis}
|
severity=${ifDefined(detail.severity)}
|
||||||
title=${detail.hint ?? ''}
|
title=${detail.tooltip ?? ''}
|
||||||
.icon=${detail.icon}
|
.icon=${{ icon: detail.icon }}
|
||||||
></advanced-camera-card-icon>`
|
></advanced-camera-card-icon>`
|
||||||
: ''}
|
: ''}
|
||||||
<span title=${detail.title}>${detail.title}</span>
|
<span title=${detail.text}>${detail.text}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,24 +9,27 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../../../camera-manager/manager';
|
import { CameraManager } from '../../../camera-manager/manager';
|
||||||
|
import { dispatchActionExecutionRequest } from '../../../card-controller/actions/utils/execution-request';
|
||||||
import { ViewItemManager } from '../../../card-controller/view/item-manager';
|
import { ViewItemManager } from '../../../card-controller/view/item-manager';
|
||||||
import { ViewManagerEpoch } from '../../../card-controller/view/types';
|
import { ViewManagerEpoch } from '../../../card-controller/view/types';
|
||||||
import {
|
import {
|
||||||
MediaDetailsController,
|
MediaDetailsController,
|
||||||
OverlayControlsContext,
|
NotificationControlsContext,
|
||||||
} from '../../../components-lib/media/details-controller';
|
} from '../../../components-lib/media/details-controller';
|
||||||
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
|
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
|
||||||
import { HomeAssistant } from '../../../ha/types';
|
import { HomeAssistant } from '../../../ha/types';
|
||||||
import { localize } from '../../../localize/localize';
|
import { localize } from '../../../localize/localize';
|
||||||
import thumbnailFeatureStyle from '../../../scss/thumbnail-feature.scss';
|
import thumbnailFeatureStyle from '../../../scss/thumbnail-feature.scss';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../../utils/action';
|
import {
|
||||||
|
createNotificationAction,
|
||||||
|
stopEventFromActivatingCardWideActions,
|
||||||
|
} from '../../../utils/action';
|
||||||
import {
|
import {
|
||||||
downloadMedia,
|
downloadMedia,
|
||||||
navigateToTimeline,
|
navigateToTimeline,
|
||||||
toggleFavorite,
|
toggleFavorite,
|
||||||
toggleReviewed,
|
toggleReviewed,
|
||||||
} from '../../../utils/media-actions';
|
} from '../../../utils/media-actions';
|
||||||
import { dispatchShowOverlayMessageEvent } from '../../../utils/overlay-message';
|
|
||||||
import { ViewItem } from '../../../view/item';
|
import { ViewItem } from '../../../view/item';
|
||||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||||
import '../../icon.js';
|
import '../../icon.js';
|
||||||
@@ -80,7 +83,7 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getControlContext(): OverlayControlsContext {
|
private _getControlContext(): NotificationControlsContext {
|
||||||
return {
|
return {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
viewItemManager: this.viewItemManager,
|
viewItemManager: this.viewItemManager,
|
||||||
@@ -207,10 +210,13 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
|||||||
stopEventFromActivatingCardWideActions(ev);
|
stopEventFromActivatingCardWideActions(ev);
|
||||||
const detailsController = new MediaDetailsController();
|
const detailsController = new MediaDetailsController();
|
||||||
detailsController.calculate(this.cameraManager, this.item);
|
detailsController.calculate(this.cameraManager, this.item);
|
||||||
dispatchShowOverlayMessageEvent(
|
dispatchActionExecutionRequest(this, {
|
||||||
this,
|
actions: [
|
||||||
detailsController.getMessage(this._getControlContext()),
|
createNotificationAction(
|
||||||
);
|
detailsController.getNotification(this._getControlContext()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
></advanced-camera-card-icon>`
|
></advanced-camera-card-icon>`
|
||||||
: ''}
|
: ''}
|
||||||
|
|||||||
@@ -19,9 +19,26 @@ import { substreamSelectActionConfigSchema } from './custom/substream-select';
|
|||||||
import { viewActionConfigSchema } from './custom/view';
|
import { viewActionConfigSchema } from './custom/view';
|
||||||
import { stockActionSchema } from './stock/types';
|
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
|
// 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<
|
export type StatusBarActionConfig = z.infer<
|
||||||
typeof advancedCameraCardCustomActionsBaseSchema
|
typeof advancedCameraCardCustomActionsBaseSchema
|
||||||
> & {
|
> & {
|
||||||
@@ -46,6 +63,7 @@ const advancedCameraCardCustomActionSchema = z.union([
|
|||||||
internalCallbackActionConfigSchema,
|
internalCallbackActionConfigSchema,
|
||||||
logActionConfigSchema,
|
logActionConfigSchema,
|
||||||
mediaPlayerActionConfigSchema,
|
mediaPlayerActionConfigSchema,
|
||||||
|
notificationActionConfigSchema,
|
||||||
ptzActionConfigSchema,
|
ptzActionConfigSchema,
|
||||||
ptzControlsActionConfigSchema,
|
ptzControlsActionConfigSchema,
|
||||||
ptzDigitalActionConfigSchema,
|
ptzDigitalActionConfigSchema,
|
||||||
@@ -66,6 +84,7 @@ export const actionConfigSchema = z.union([
|
|||||||
advancedCameraCardCustomActionSchema,
|
advancedCameraCardCustomActionSchema,
|
||||||
]);
|
]);
|
||||||
export type ActionConfig = z.infer<typeof actionConfigSchema>;
|
export type ActionConfig = z.infer<typeof actionConfigSchema>;
|
||||||
|
|
||||||
export const actionsBaseSchema = z
|
export const actionsBaseSchema = z
|
||||||
.object({
|
.object({
|
||||||
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
@@ -90,6 +109,38 @@ export const actionsSchema = z.object({
|
|||||||
actions: actionsBaseSchema.optional(),
|
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
|
// Status Bar Elements
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export const statusBarConfigDefault = {
|
|||||||
severity: statusBarItemDefault,
|
severity: statusBarItemDefault,
|
||||||
technology: statusBarItemDefault,
|
technology: statusBarItemDefault,
|
||||||
title: statusBarItemDefault,
|
title: statusBarItemDefault,
|
||||||
|
upgrade: statusBarItemDefault,
|
||||||
},
|
},
|
||||||
position: 'bottom' as const,
|
position: 'bottom' as const,
|
||||||
style: 'popup' as const,
|
style: 'popup' as const,
|
||||||
@@ -53,6 +54,7 @@ export const statusBarConfigSchema = z
|
|||||||
),
|
),
|
||||||
severity: statusBarItemBaseSchema.default(statusBarConfigDefault.items.severity),
|
severity: statusBarItemBaseSchema.default(statusBarConfigDefault.items.severity),
|
||||||
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
|
title: statusBarItemBaseSchema.default(statusBarConfigDefault.items.title),
|
||||||
|
upgrade: statusBarItemBaseSchema.default(statusBarConfigDefault.items.upgrade),
|
||||||
})
|
})
|
||||||
.default(statusBarConfigDefault.items),
|
.default(statusBarConfigDefault.items),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3110,6 +3110,7 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
|
|||||||
${this._renderStatusBarItem('resolution') /* */}
|
${this._renderStatusBarItem('resolution') /* */}
|
||||||
${this._renderStatusBarItem('technology') /* */}
|
${this._renderStatusBarItem('technology') /* */}
|
||||||
${this._renderStatusBarItem('engine') /* */}
|
${this._renderStatusBarItem('engine') /* */}
|
||||||
|
${this._renderStatusBarItem('upgrade') /* */}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "Advanced Camera Card",
|
"advanced_camera_card": "Advanced Camera Card",
|
||||||
"advanced_camera_card_description": "An Advanced Camera Card",
|
"advanced_camera_card_description": "An Advanced Camera Card",
|
||||||
|
"dismiss": "Dismiss",
|
||||||
"folder": "Folder",
|
"folder": "Folder",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
"no_folder": "No folder to display",
|
"no_folder": "No folder to display",
|
||||||
@@ -557,7 +558,8 @@
|
|||||||
"priority": "Item priority",
|
"priority": "Item priority",
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
"technology": "Technology",
|
"technology": "Technology",
|
||||||
"title": "Title"
|
"title": "Title",
|
||||||
|
"upgrade": "Configuration upgrade notification"
|
||||||
},
|
},
|
||||||
"popup_seconds": "Status bar popup seconds",
|
"popup_seconds": "Status bar popup seconds",
|
||||||
"position": "Status bar position",
|
"position": "Status bar position",
|
||||||
@@ -821,6 +823,12 @@
|
|||||||
"media_viewer": {
|
"media_viewer": {
|
||||||
"unseekable": "Seek time not found in media"
|
"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": {
|
"thumbnail": {
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"download": "Download media",
|
"download": "Download media",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: $z-index-overlay-message;
|
z-index: $z-index-notification;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.notification {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -30,23 +30,22 @@
|
|||||||
padding-right: 48px;
|
padding-right: 48px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--advanced-camera-card-overlay-message-scrollbar-color)
|
scrollbar-color: var(--advanced-camera-card-notification-scrollbar-color) transparent;
|
||||||
transparent;
|
|
||||||
|
|
||||||
// Glassmorphism background
|
// Glassmorphism background
|
||||||
background: var(--advanced-camera-card-overlay-message-background);
|
background: var(--advanced-camera-card-notification-background);
|
||||||
backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
|
backdrop-filter: var(--advanced-camera-card-notification-backdrop-filter);
|
||||||
-webkit-backdrop-filter: var(--advanced-camera-card-overlay-message-backdrop-filter);
|
-webkit-backdrop-filter: var(--advanced-camera-card-notification-backdrop-filter);
|
||||||
|
|
||||||
// Borders & Edge highlights
|
// Borders & Edge highlights
|
||||||
border: var(--advanced-camera-card-overlay-message-border);
|
border: var(--advanced-camera-card-notification-border);
|
||||||
border-top: var(--advanced-camera-card-overlay-message-border-top);
|
border-top: var(--advanced-camera-card-notification-border-top);
|
||||||
border-radius: var(--advanced-camera-card-border-radius-final);
|
border-radius: var(--advanced-camera-card-border-radius-final);
|
||||||
|
|
||||||
// Shadows
|
// Shadows
|
||||||
box-shadow: var(
|
box-shadow: var(
|
||||||
--advanced-camera-card-box-shadow-override,
|
--advanced-camera-card-box-shadow-override,
|
||||||
var(--advanced-camera-card-overlay-message-box-shadow)
|
var(--advanced-camera-card-notification-box-shadow)
|
||||||
);
|
);
|
||||||
|
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exit animation
|
// Exit animation
|
||||||
.message.exiting {
|
.notification.exiting {
|
||||||
animation: slideDown 0.25s ease-out forwards;
|
animation: slideDown 0.25s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,12 +98,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--advanced-camera-card-overlay-message-icon-color);
|
color: var(--advanced-camera-card-notification-icon-color);
|
||||||
filter: drop-shadow(
|
filter: drop-shadow(
|
||||||
0 0 8px
|
0 0 8px
|
||||||
color-mix(
|
color-mix(
|
||||||
in oklab,
|
in oklab,
|
||||||
var(--advanced-camera-card-overlay-message-icon-color),
|
var(--advanced-camera-card-notification-icon-color),
|
||||||
transparent 60%
|
transparent 60%
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
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;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@
|
|||||||
|
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-close-color),
|
var(--advanced-camera-card-notification-close-color),
|
||||||
transparent 92%
|
transparent 92%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -150,7 +149,7 @@
|
|||||||
|
|
||||||
color: color-mix(
|
color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-close-color),
|
var(--advanced-camera-card-notification-close-color),
|
||||||
transparent 40%
|
transparent 40%
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -166,17 +165,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base control hover color transition (from dimmed to full).
|
// Base control hover color transition (from dimmed to full).
|
||||||
&:not([class^='emphasis-']):hover {
|
&:not([class^='severity-']):hover {
|
||||||
color: var(--advanced-camera-card-overlay-message-close-color);
|
color: var(--advanced-camera-card-notification-close-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.emphasis-low {
|
&.severity-low {
|
||||||
color: var(--advanced-camera-card-foreground-severity-low-color);
|
color: var(--advanced-camera-card-foreground-severity-low-color);
|
||||||
}
|
}
|
||||||
&.emphasis-medium {
|
&.severity-medium {
|
||||||
color: var(--advanced-camera-card-foreground-severity-medium-color);
|
color: var(--advanced-camera-card-foreground-severity-medium-color);
|
||||||
}
|
}
|
||||||
&.emphasis-high {
|
&.severity-high {
|
||||||
color: var(--advanced-camera-card-foreground-severity-high-color);
|
color: var(--advanced-camera-card-foreground-severity-high-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,12 +193,12 @@
|
|||||||
|
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-close-color),
|
var(--advanced-camera-card-notification-close-color),
|
||||||
transparent 95%
|
transparent 95%
|
||||||
);
|
);
|
||||||
color: color-mix(
|
color: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-close-color),
|
var(--advanced-camera-card-notification-close-color),
|
||||||
transparent 40%
|
transparent 40%
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,10 +209,10 @@
|
|||||||
.close:hover {
|
.close:hover {
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-close-color),
|
var(--advanced-camera-card-notification-close-color),
|
||||||
transparent 90%
|
transparent 90%
|
||||||
);
|
);
|
||||||
color: var(--advanced-camera-card-overlay-message-close-color);
|
color: var(--advanced-camera-card-notification-close-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.close advanced-camera-card-icon {
|
.close advanced-camera-card-icon {
|
||||||
@@ -233,7 +232,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--advanced-camera-card-overlay-message-text-color);
|
color: var(--advanced-camera-card-notification-text-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
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);
|
color: var(--advanced-camera-card-foreground-severity-low-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&.emphasis-medium advanced-camera-card-icon {
|
&.severity-medium advanced-camera-card-icon {
|
||||||
color: var(--advanced-camera-card-foreground-severity-medium-color);
|
color: var(--advanced-camera-card-foreground-severity-medium-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&.emphasis-high advanced-camera-card-icon {
|
&.severity-high advanced-camera-card-icon {
|
||||||
color: var(--advanced-camera-card-foreground-severity-high-color);
|
color: var(--advanced-camera-card-foreground-severity-high-color);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -282,10 +281,10 @@
|
|||||||
border-top: 1px solid
|
border-top: 1px solid
|
||||||
color-mix(
|
color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
var(--advanced-camera-card-overlay-message-text-color),
|
var(--advanced-camera-card-notification-text-color),
|
||||||
transparent 80%
|
transparent 80%
|
||||||
);
|
);
|
||||||
color: var(--advanced-camera-card-overlay-message-text-color);
|
color: var(--advanced-camera-card-notification-text-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
|
|
||||||
.item.action {
|
.item.action {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.item,
|
img.item,
|
||||||
|
|||||||
+12
-12
@@ -286,24 +286,24 @@
|
|||||||
transparent 20%
|
transparent 20%
|
||||||
);
|
);
|
||||||
|
|
||||||
/******************
|
/**************
|
||||||
* Overlay Message
|
* Notification
|
||||||
******************/
|
**************/
|
||||||
--advanced-camera-card-overlay-message-background: linear-gradient(
|
--advanced-camera-card-notification-background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(35, 35, 35, 0.75) 0%,
|
rgba(35, 35, 35, 0.75) 0%,
|
||||||
rgba(15, 15, 15, 0.65) 100%
|
rgba(15, 15, 15, 0.65) 100%
|
||||||
);
|
);
|
||||||
--advanced-camera-card-overlay-message-backdrop-filter: blur(20px) saturate(180%);
|
--advanced-camera-card-notification-backdrop-filter: blur(20px) saturate(180%);
|
||||||
--advanced-camera-card-overlay-message-border: 1px solid rgba(255, 255, 255, 0.1);
|
--advanced-camera-card-notification-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-notification-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-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);
|
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-notification-text-color: #ffffff;
|
||||||
--advanced-camera-card-overlay-message-icon-color: var(
|
--advanced-camera-card-notification-icon-color: var(
|
||||||
--advanced-camera-card-foreground-primary
|
--advanced-camera-card-foreground-primary
|
||||||
);
|
);
|
||||||
--advanced-camera-card-overlay-message-scrollbar-color: rgba(255, 255, 255, 0.2);
|
--advanced-camera-card-notification-scrollbar-color: rgba(255, 255, 255, 0.2);
|
||||||
--advanced-camera-card-overlay-message-close-color: #ffffff;
|
--advanced-camera-card-notification-close-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -522,20 +522,20 @@
|
|||||||
// Menu border colors need to re-adjust based on redefined variable.
|
// Menu border colors need to re-adjust based on redefined variable.
|
||||||
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
|
--wa-color-surface-border: var(--ha-color-border-neutral-quiet);
|
||||||
|
|
||||||
/******************
|
/**************
|
||||||
* Overlay Message
|
* Notification
|
||||||
******************/
|
**************/
|
||||||
--advanced-camera-card-overlay-message-background: linear-gradient(
|
--advanced-camera-card-notification-background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(255, 255, 255, 0.85) 0%,
|
rgba(255, 255, 255, 0.85) 0%,
|
||||||
rgba(235, 235, 235, 0.75) 100%
|
rgba(235, 235, 235, 0.75) 100%
|
||||||
);
|
);
|
||||||
--advanced-camera-card-overlay-message-text-color: var(--primary-text-color);
|
--advanced-camera-card-notification-text-color: var(--primary-text-color);
|
||||||
--advanced-camera-card-overlay-message-border: 1px solid rgba(0, 0, 0, 0.3);
|
--advanced-camera-card-notification-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-notification-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-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);
|
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-notification-scrollbar-color: rgba(0, 0, 0, 0.5);
|
||||||
--advanced-camera-card-overlay-message-close-color: #000000;
|
--advanced-camera-card-notification-close-color: #000000;
|
||||||
--app-header-text-color: white;
|
--app-header-text-color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ $z-index-loading: 6;
|
|||||||
|
|
||||||
// Need menu to render above drawer (so the drawer button is below menu-submenus)
|
// Need menu to render above drawer (so the drawer button is below menu-submenus)
|
||||||
$z-index-menu: 4;
|
$z-index-menu: 4;
|
||||||
$z-index-overlay-message: 4;
|
$z-index-notification: 4;
|
||||||
$z-index-drawer: 3;
|
$z-index-drawer: 3;
|
||||||
$z-index-status-bar: 2;
|
$z-index-status-bar: 2;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { EffectOptions } from './card-controller/effects/types';
|
import type { EffectOptions } from './card-controller/effects/types';
|
||||||
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
||||||
import { Severity } from './severity';
|
|
||||||
|
|
||||||
// UI-facing media types for galleries and views.
|
// UI-facing media types for galleries and views.
|
||||||
export const VIEW_MEDIA_TYPES = ['clips', 'snapshots', 'recordings', 'reviews'] as const;
|
export const VIEW_MEDIA_TYPES = ['clips', 'snapshots', 'recordings', 'reviews'] as const;
|
||||||
@@ -64,24 +63,6 @@ export interface Message {
|
|||||||
url?: MessageURL;
|
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 & {
|
export type WebkitHTMLVideoElement = HTMLVideoElement & {
|
||||||
webkitDisplayingFullscreen: boolean;
|
webkitDisplayingFullscreen: boolean;
|
||||||
webkitSupportsFullscreen: boolean;
|
webkitSupportsFullscreen: boolean;
|
||||||
@@ -182,21 +163,6 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
|
|||||||
'trigger',
|
'trigger',
|
||||||
] as const;
|
] 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 {
|
export interface Interaction {
|
||||||
action: string;
|
action: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-2
@@ -28,10 +28,12 @@ import { SetReviewActionConfig } from '../config/schema/actions/custom/set-revie
|
|||||||
import { SubstreamSelectActionConfig } from '../config/schema/actions/custom/substream-select.js';
|
import { SubstreamSelectActionConfig } from '../config/schema/actions/custom/substream-select.js';
|
||||||
import { ViewActionConfig } from '../config/schema/actions/custom/view.js';
|
import { ViewActionConfig } from '../config/schema/actions/custom/view.js';
|
||||||
import { PerformActionActionConfig } from '../config/schema/actions/stock/perform-action.js';
|
import { PerformActionActionConfig } from '../config/schema/actions/stock/perform-action.js';
|
||||||
|
import type { Notification } from '../config/schema/actions/types.js';
|
||||||
import {
|
import {
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
ActionsConfig,
|
Actions,
|
||||||
AdvancedCameraCardCustomActionConfig,
|
AdvancedCameraCardCustomActionConfig,
|
||||||
|
NotificationActionConfig,
|
||||||
} from '../config/schema/actions/types.js';
|
} from '../config/schema/actions/types.js';
|
||||||
import { AdvancedCameraCardUserSpecifiedView } from '../config/schema/common/const.js';
|
import { AdvancedCameraCardUserSpecifiedView } from '../config/schema/common/const.js';
|
||||||
import { PTZControlType } from '../config/schema/common/controls/ptz.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').
|
* Get an action configuration given a config and an interaction (e.g. 'tap').
|
||||||
* @param interaction The interaction: `tap`, `hold` or `double_tap`
|
* @param interaction The interaction: `tap`, `hold` or `double_tap`
|
||||||
@@ -278,7 +294,7 @@ export function createSetReviewAction(reviewed?: boolean): SetReviewActionConfig
|
|||||||
*/
|
*/
|
||||||
export function getActionConfigGivenAction(
|
export function getActionConfigGivenAction(
|
||||||
interaction?: string,
|
interaction?: string,
|
||||||
config?: ActionsConfig | null,
|
config?: Actions | null,
|
||||||
): ActionConfig | ActionConfig[] | null {
|
): ActionConfig | ActionConfig[] | null {
|
||||||
if (!interaction || !config) {
|
if (!interaction || !config) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { fireAdvancedCameraCardEvent } from './fire-advanced-camera-card-event.js';
|
||||||
|
|
||||||
|
export function dispatchDismissNotificationEvent(element: HTMLElement): void {
|
||||||
|
fireAdvancedCameraCardEvent(element, 'notification:dismiss');
|
||||||
|
}
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,7 @@ describe('InfoAction', () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(api.getOverlayMessageManager().setMessage).toBeCalled();
|
expect(api.getNotificationManager().setNotification).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not handle info action without media', async () => {
|
it('should not handle info action without media', async () => {
|
||||||
@@ -45,6 +45,6 @@ describe('InfoAction', () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(api.getOverlayMessageManager().setMessage).not.toBeCalled();
|
expect(api.getNotificationManager().setNotification).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { NotificationAction } from '../../../../src/card-controller/actions/actions/notification';
|
||||||
|
import { createCardAPI } from '../../../test-utils';
|
||||||
|
|
||||||
|
describe('NotificationAction', () => {
|
||||||
|
it('should set notification on manager', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const notification = {
|
||||||
|
heading: { text: 'Test Heading' },
|
||||||
|
text: 'Test text',
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new NotificationAction(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
advanced_camera_card_action: 'notification',
|
||||||
|
notification,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await action.execute(api);
|
||||||
|
|
||||||
|
expect(api.getNotificationManager().setNotification).toHaveBeenCalledWith(
|
||||||
|
notification,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,9 +22,10 @@ import { MoreInfoAction } from '../../../src/card-controller/actions/actions/mor
|
|||||||
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
|
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
|
||||||
import { NavigateAction } from '../../../src/card-controller/actions/actions/navigate';
|
import { NavigateAction } from '../../../src/card-controller/actions/actions/navigate';
|
||||||
import { NoneAction } from '../../../src/card-controller/actions/actions/none';
|
import { NoneAction } from '../../../src/card-controller/actions/actions/none';
|
||||||
|
import { NotificationAction } from '../../../src/card-controller/actions/actions/notification';
|
||||||
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
|
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
|
||||||
import { PIPAction } from '../../../src/card-controller/actions/actions/pip';
|
|
||||||
import { PerformActionAction } from '../../../src/card-controller/actions/actions/perform-action';
|
import { PerformActionAction } from '../../../src/card-controller/actions/actions/perform-action';
|
||||||
|
import { PIPAction } from '../../../src/card-controller/actions/actions/pip';
|
||||||
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
||||||
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
||||||
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
||||||
@@ -126,6 +127,13 @@ describe('ActionFactory', () => {
|
|||||||
},
|
},
|
||||||
LogAction,
|
LogAction,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
advanced_camera_card_action: 'notification' as const,
|
||||||
|
notification: { text: 'test' },
|
||||||
|
},
|
||||||
|
NotificationAction,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
advanced_camera_card_action: 'media_player' as const,
|
advanced_camera_card_action: 'media_player' as const,
|
||||||
|
|||||||
@@ -121,6 +121,27 @@ describe('ConfigManager', () => {
|
|||||||
expect(manager.getConfig()).toBeNull();
|
expect(manager.getConfig()).toBeNull();
|
||||||
expect(manager.getNonOverriddenConfig()).toBeNull();
|
expect(manager.getNonOverriddenConfig()).toBeNull();
|
||||||
expect(manager.getRawConfig()).toBeNull();
|
expect(manager.getRawConfig()).toBeNull();
|
||||||
|
expect(manager.isUpgradeable()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isUpgradeable', () => {
|
||||||
|
it('should return true for upgradeable config', () => {
|
||||||
|
const manager = new ConfigManager(createCardAPI());
|
||||||
|
manager.setConfig({
|
||||||
|
type: 'custom:frigate-card',
|
||||||
|
cameras: [TEST_CAMERAS.OFFICE],
|
||||||
|
});
|
||||||
|
expect(manager.isUpgradeable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-upgradeable config', () => {
|
||||||
|
const manager = new ConfigManager(createCardAPI());
|
||||||
|
manager.setConfig({
|
||||||
|
type: 'custom:advanced-camera-card',
|
||||||
|
cameras: [TEST_CAMERAS.OFFICE],
|
||||||
|
});
|
||||||
|
expect(manager.isUpgradeable()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should successfully parse basic config', () => {
|
it('should successfully parse basic config', () => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { MediaLoadedInfoManager } from '../../src/card-controller/media-info-man
|
|||||||
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||||
import { MessageManager } from '../../src/card-controller/message-manager';
|
import { MessageManager } from '../../src/card-controller/message-manager';
|
||||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||||
import { OverlayMessageManager } from '../../src/card-controller/overlay-message-manager';
|
import { NotificationManager } from '../../src/card-controller/notification-manager';
|
||||||
import { PIPManager } from '../../src/card-controller/pip-manager';
|
import { PIPManager } from '../../src/card-controller/pip-manager';
|
||||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||||
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
import { StatusBarItemManager } from '../../src/card-controller/status-bar-item-manager';
|
||||||
@@ -54,7 +54,7 @@ vi.mock('../../src/card-controller/media-info-manager');
|
|||||||
vi.mock('../../src/card-controller/media-player-manager');
|
vi.mock('../../src/card-controller/media-player-manager');
|
||||||
vi.mock('../../src/card-controller/message-manager');
|
vi.mock('../../src/card-controller/message-manager');
|
||||||
vi.mock('../../src/card-controller/microphone-manager');
|
vi.mock('../../src/card-controller/microphone-manager');
|
||||||
vi.mock('../../src/card-controller/overlay-message-manager');
|
vi.mock('../../src/card-controller/notification-manager');
|
||||||
vi.mock('../../src/card-controller/pip-manager');
|
vi.mock('../../src/card-controller/pip-manager');
|
||||||
vi.mock('../../src/card-controller/query-string-manager');
|
vi.mock('../../src/card-controller/query-string-manager');
|
||||||
vi.mock('../../src/card-controller/status-bar-item-manager');
|
vi.mock('../../src/card-controller/status-bar-item-manager');
|
||||||
@@ -226,9 +226,9 @@ describe('CardController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getOverlayMessageManager', () => {
|
it('getNotificationManager', () => {
|
||||||
expect(createController().getOverlayMessageManager()).toBe(
|
expect(createController().getNotificationManager()).toBe(
|
||||||
vi.mocked(OverlayMessageManager).mock.instances[0],
|
vi.mocked(NotificationManager).mock.instances[0],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
||||||
|
import { NotificationManager } from '../../src/card-controller/notification-manager';
|
||||||
|
import { CardNotificationAPI } from '../../src/card-controller/types';
|
||||||
|
|
||||||
|
describe('NotificationManager', () => {
|
||||||
|
const cardElementManager = mock<CardElementManager>();
|
||||||
|
const api = mock<CardNotificationAPI>();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
api.getCardElementManager.mockReturnValue(cardElementManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be constructed', () => {
|
||||||
|
const manager = new NotificationManager(api);
|
||||||
|
expect(manager).toBeDefined();
|
||||||
|
expect(manager.getNotification()).toBeNull();
|
||||||
|
expect(manager.hasNotification()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set and get notification', () => {
|
||||||
|
const manager = new NotificationManager(api);
|
||||||
|
const notification = { text: 'foo' };
|
||||||
|
manager.setNotification(notification);
|
||||||
|
|
||||||
|
expect(manager.getNotification()).toBe(notification);
|
||||||
|
expect(manager.hasNotification()).toBeTruthy();
|
||||||
|
expect(cardElementManager.update).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset notification', () => {
|
||||||
|
const manager = new NotificationManager(api);
|
||||||
|
manager.setNotification({ text: 'foo' });
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
manager.reset();
|
||||||
|
|
||||||
|
expect(manager.getNotification()).toBeNull();
|
||||||
|
expect(manager.hasNotification()).toBeFalsy();
|
||||||
|
expect(cardElementManager.update).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update if reset is called with no notification', () => {
|
||||||
|
const manager = new NotificationManager(api);
|
||||||
|
manager.reset();
|
||||||
|
|
||||||
|
expect(cardElementManager.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { CardElementManager } from '../../src/card-controller/card-element-manager';
|
|
||||||
import { OverlayMessageManager } from '../../src/card-controller/overlay-message-manager';
|
|
||||||
import { CardOverlayMessageAPI } from '../../src/card-controller/types';
|
|
||||||
|
|
||||||
describe('OverlayMessageManager', () => {
|
|
||||||
const cardElementManager = mock<CardElementManager>();
|
|
||||||
const api = mock<CardOverlayMessageAPI>();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
api.getCardElementManager.mockReturnValue(cardElementManager);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be constructed', () => {
|
|
||||||
const manager = new OverlayMessageManager(api);
|
|
||||||
expect(manager).toBeDefined();
|
|
||||||
expect(manager.getMessage()).toBeNull();
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set and get message', () => {
|
|
||||||
const manager = new OverlayMessageManager(api);
|
|
||||||
const message = { text: 'foo' };
|
|
||||||
manager.setMessage(message);
|
|
||||||
|
|
||||||
expect(manager.getMessage()).toBe(message);
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
expect(cardElementManager.update).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset message', () => {
|
|
||||||
const manager = new OverlayMessageManager(api);
|
|
||||||
manager.setMessage({ text: 'foo' });
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
manager.reset();
|
|
||||||
|
|
||||||
expect(manager.getMessage()).toBeNull();
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
expect(cardElementManager.update).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update if reset is called with no message', () => {
|
|
||||||
const manager = new OverlayMessageManager(api);
|
|
||||||
manager.reset();
|
|
||||||
|
|
||||||
expect(cardElementManager.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -241,6 +241,56 @@ describe('StatusBarItemManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('upgrade', () => {
|
||||||
|
it('should show upgrade item when upgradeable', () => {
|
||||||
|
const manager = new StatusBarItemManager(createCardAPI());
|
||||||
|
|
||||||
|
const items = manager.calculateItems({
|
||||||
|
isUpgradeable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(items).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'custom:advanced-camera-card-status-bar-icon' as const,
|
||||||
|
icon: 'mdi:update',
|
||||||
|
severity: 'medium',
|
||||||
|
actions: expect.objectContaining({
|
||||||
|
tap_action: expect.objectContaining({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
advanced_camera_card_action: 'notification',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show upgrade item when not upgradeable', () => {
|
||||||
|
const manager = new StatusBarItemManager(createCardAPI());
|
||||||
|
|
||||||
|
const items = manager.calculateItems({
|
||||||
|
isUpgradeable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(items).not.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
icon: 'mdi:update',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show upgrade item by default', () => {
|
||||||
|
const manager = new StatusBarItemManager(createCardAPI());
|
||||||
|
|
||||||
|
const items = manager.calculateItems();
|
||||||
|
|
||||||
|
expect(items).not.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
icon: 'mdi:update',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('severity', () => {
|
describe('severity', () => {
|
||||||
it('should have severity in a viewer view', () => {
|
it('should have severity in a viewer view', () => {
|
||||||
const manager = new StatusBarItemManager(createCardAPI());
|
const manager = new StatusBarItemManager(createCardAPI());
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { CameraManager } from '../../../src/camera-manager/manager';
|
import { CameraManager } from '../../../src/camera-manager/manager';
|
||||||
|
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
||||||
|
import { CardController } from '../../../src/card-controller/controller';
|
||||||
import { ViewItemManager } from '../../../src/card-controller/view/item-manager';
|
import { ViewItemManager } from '../../../src/card-controller/view/item-manager';
|
||||||
import { ViewManagerEpoch } from '../../../src/card-controller/view/types';
|
import { ViewManagerEpoch } from '../../../src/card-controller/view/types';
|
||||||
import {
|
import {
|
||||||
MediaDetailsController,
|
MediaDetailsController,
|
||||||
OverlayControlsContext,
|
NotificationControlsContext,
|
||||||
} from '../../../src/components-lib/media/details-controller';
|
} from '../../../src/components-lib/media/details-controller';
|
||||||
import { OverlayMessageControl } from '../../../src/types';
|
import { NotificationControl } from '../../../src/config/schema/actions/types';
|
||||||
import { formatDateAndTime } from '../../../src/utils/basic';
|
import { formatDateAndTime } from '../../../src/utils/basic';
|
||||||
|
import { downloadMedia, navigateToTimeline } from '../../../src/utils/media-actions';
|
||||||
import { ViewFolder, ViewMediaType } from '../../../src/view/item';
|
import { ViewFolder, ViewMediaType } from '../../../src/view/item';
|
||||||
import { createCardAPI, createFolder, TestViewMedia } from '../../test-utils';
|
import { createCardAPI, createFolder, TestViewMedia } from '../../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../src/utils/media-actions', async (importOriginal) => ({
|
||||||
|
...((await importOriginal()) as object),
|
||||||
|
downloadMedia: vi.fn(),
|
||||||
|
navigateToTimeline: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function executeControlAction(
|
||||||
|
control: NotificationControl,
|
||||||
|
api: CardController,
|
||||||
|
): Promise<void> {
|
||||||
|
const tapAction = control.actions?.tap_action;
|
||||||
|
|
||||||
|
assert(tapAction && !Array.isArray(tapAction));
|
||||||
|
const action = new ActionFactory().createAction({}, tapAction);
|
||||||
|
|
||||||
|
await action?.execute(api);
|
||||||
|
}
|
||||||
|
|
||||||
describe('MediaDetailsController', () => {
|
describe('MediaDetailsController', () => {
|
||||||
describe('should set heading', () => {
|
describe('should set heading', () => {
|
||||||
it('should set heading on event with what, tags and score', () => {
|
it('should set heading on event with what, tags and score', () => {
|
||||||
@@ -24,7 +45,7 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getHeading()?.title).toBe('Person, Car: Tag1, Tag2 50.00%');
|
expect(controller.getHeading()?.text).toBe('Person, Car: Tag1, Tag2 50.00%');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set heading on event with tags', () => {
|
it('should set heading on event with tags', () => {
|
||||||
@@ -34,7 +55,7 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getHeading()?.title).toBe('Tag1, Tag2');
|
expect(controller.getHeading()?.text).toBe('Tag1, Tag2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set heading on event with what', () => {
|
it('should set heading on event with what', () => {
|
||||||
@@ -44,7 +65,7 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getHeading()?.title).toBe('Person, Car');
|
expect(controller.getHeading()?.text).toBe('Person, Car');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set null heading on event with no other information', () => {
|
it('should set null heading on event with no other information', () => {
|
||||||
@@ -73,7 +94,7 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(cameraManager, item);
|
controller.calculate(cameraManager, item);
|
||||||
expect(controller.getHeading()?.title).toBe('Camera Title');
|
expect(controller.getHeading()?.text).toBe('Camera Title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set heading on recording without camera metadata', () => {
|
it('should set heading on recording without camera metadata', () => {
|
||||||
@@ -106,9 +127,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: 'Test Event',
|
text: 'Test Event',
|
||||||
icon: { icon: 'mdi:rename' },
|
icon: 'mdi:rename',
|
||||||
hint: 'Title',
|
tooltip: 'Title',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,7 +142,7 @@ describe('MediaDetailsController', () => {
|
|||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toEqual([
|
expect(controller.getDetails()).toEqual([
|
||||||
{
|
{
|
||||||
title: 'Test Event',
|
text: 'Test Event',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -136,7 +157,7 @@ describe('MediaDetailsController', () => {
|
|||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).not.toContainEqual(
|
expect(controller.getDetails()).not.toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: 'Test Event',
|
text: 'Test Event',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -153,9 +174,9 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
// Use formatDateAndTime to generate expected value (formats in local time with seconds)
|
// Use formatDateAndTime to generate expected value (formats in local time with seconds)
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: formatDateAndTime(startTime, true),
|
text: formatDateAndTime(startTime, true),
|
||||||
hint: 'Start',
|
tooltip: 'Start',
|
||||||
icon: { icon: 'mdi:calendar-clock-outline' },
|
icon: 'mdi:calendar-clock-outline',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,9 +190,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: '1m 0s',
|
text: '1m 0s',
|
||||||
hint: 'Duration',
|
tooltip: 'Duration',
|
||||||
icon: { icon: 'mdi:clock-outline' },
|
icon: 'mdi:clock-outline',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -185,9 +206,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: 'In Progress',
|
text: 'In Progress',
|
||||||
hint: 'Duration',
|
tooltip: 'Duration',
|
||||||
icon: { icon: 'mdi:clock-outline' },
|
icon: 'mdi:clock-outline',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,9 +222,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: '1m 0s In Progress',
|
text: '1m 0s In Progress',
|
||||||
hint: 'Duration',
|
tooltip: 'Duration',
|
||||||
icon: { icon: 'mdi:clock-outline' },
|
icon: 'mdi:clock-outline',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -222,9 +243,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(cameraManager, item);
|
controller.calculate(cameraManager, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: 'Camera Title',
|
text: 'Camera Title',
|
||||||
hint: 'Camera',
|
tooltip: 'Camera',
|
||||||
icon: { icon: 'mdi:cctv' },
|
icon: 'mdi:cctv',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,9 +258,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: 'Where1, Where2',
|
text: 'Where1, Where2',
|
||||||
hint: 'Where',
|
tooltip: 'Where',
|
||||||
icon: { icon: 'mdi:map-marker-outline' },
|
icon: 'mdi:map-marker-outline',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -252,9 +273,9 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: 'Tag1, Tag2',
|
text: 'Tag1, Tag2',
|
||||||
hint: 'Tag',
|
tooltip: 'Tag',
|
||||||
icon: { icon: 'mdi:tag' },
|
icon: 'mdi:tag',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,9 +288,9 @@ describe('MediaDetailsController', () => {
|
|||||||
|
|
||||||
// Use format() to generate expected value (formats in local time)
|
// Use format() to generate expected value (formats in local time)
|
||||||
expect(controller.getDetails()).toContainEqual({
|
expect(controller.getDetails()).toContainEqual({
|
||||||
title: format(seekTime, 'HH:mm:ss'),
|
text: format(seekTime, 'HH:mm:ss'),
|
||||||
hint: 'Seek',
|
tooltip: 'Seek',
|
||||||
icon: { icon: 'mdi:clock-fast' },
|
icon: 'mdi:clock-fast',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should set heading on review', () => {
|
it('should set heading on review', () => {
|
||||||
@@ -282,10 +303,10 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
const heading = controller.getHeading();
|
const heading = controller.getHeading();
|
||||||
expect(heading?.title).toBe('Review Title');
|
expect(heading?.text).toBe('Review Title');
|
||||||
expect(heading?.emphasis).toBe('high');
|
expect(heading?.severity).toBe('high');
|
||||||
expect(heading?.icon).toEqual({ icon: 'mdi:circle-medium' });
|
expect(heading?.icon).toBe('mdi:circle-medium');
|
||||||
expect(heading?.hint).toBe('Severity: High');
|
expect(heading?.tooltip).toBe('Severity: High');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set heading on review without severity', () => {
|
it('should set heading on review without severity', () => {
|
||||||
@@ -298,8 +319,8 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
const heading = controller.getHeading();
|
const heading = controller.getHeading();
|
||||||
expect(heading?.title).toBe('Review Title');
|
expect(heading?.text).toBe('Review Title');
|
||||||
expect(heading?.emphasis).toBeUndefined();
|
expect(heading?.severity).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set null heading on review with no title', () => {
|
it('should set null heading on review with no title', () => {
|
||||||
@@ -321,12 +342,12 @@ describe('MediaDetailsController', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should get message', () => {
|
describe('should get notification', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message', () => {
|
it('should get notification', () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
what: ['person'],
|
what: ['person'],
|
||||||
@@ -336,25 +357,25 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage();
|
const notification = controller.getNotification();
|
||||||
expect(message.heading?.title).toBe('Person');
|
expect(notification.heading?.text).toBe('Person');
|
||||||
expect(message.details).toContainEqual({
|
expect(notification.details).toContainEqual({
|
||||||
title: 'Test Title',
|
text: 'Test Title',
|
||||||
});
|
});
|
||||||
expect(message.text).toBe('Test Description');
|
expect(notification.text).toBe('Test Description');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message without media', () => {
|
it('should get notification without media', () => {
|
||||||
const item = new ViewFolder(createFolder(), []);
|
const item = new ViewFolder(createFolder(), []);
|
||||||
|
|
||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage();
|
const notification = controller.getNotification();
|
||||||
expect(message.text).toBeUndefined();
|
expect(notification.text).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message with null description', () => {
|
it('should get notification with null description', () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
description: null,
|
description: null,
|
||||||
});
|
});
|
||||||
@@ -362,11 +383,11 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage();
|
const notification = controller.getNotification();
|
||||||
expect(message.text).toBeUndefined();
|
expect(notification.text).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message with controls', async () => {
|
it('should get notification with controls', async () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
mediaType: ViewMediaType.Review,
|
mediaType: ViewMediaType.Review,
|
||||||
@@ -390,45 +411,52 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage(context);
|
const notification = controller.getNotification(context);
|
||||||
const controls = message.controls;
|
const controls = notification.controls;
|
||||||
|
assert(controls);
|
||||||
expect(controls).toHaveLength(4);
|
expect(controls).toHaveLength(4);
|
||||||
|
|
||||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
||||||
// 1. Review control
|
// 1. Review control
|
||||||
expect(controls?.[0].title).toBe('Mark as reviewed');
|
expect(controls?.[0].tooltip).toBe('Mark as reviewed');
|
||||||
const reviewResult = await controls?.[0].callback?.();
|
expect(controls?.[0].dismiss).toBe(false);
|
||||||
expect(reviewResult).not.toBeNull();
|
await executeControlAction(controls[0], cardAPI);
|
||||||
|
expect(cardAPI.getNotificationManager().setNotification).toHaveBeenCalled();
|
||||||
|
|
||||||
// 1b. Review control (failure)
|
// 1b. Review control (failure)
|
||||||
|
vi.mocked(cardAPI.getNotificationManager().setNotification).mockClear();
|
||||||
viewItemManager.reviewMedia.mockRejectedValue(new Error('fail'));
|
viewItemManager.reviewMedia.mockRejectedValue(new Error('fail'));
|
||||||
|
await executeControlAction(controls[0], cardAPI);
|
||||||
const reviewFailureResult = await controls?.[0].callback?.();
|
expect(cardAPI.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
||||||
expect(reviewFailureResult).toBeNull();
|
|
||||||
|
|
||||||
// 2. Favorite control
|
// 2. Favorite control
|
||||||
expect(controls?.[1].title).toBe('Media will be indefinitely retained');
|
expect(controls?.[1].tooltip).toBe('Media will be indefinitely retained');
|
||||||
const favoriteResult = await controls?.[1].callback?.();
|
expect(controls?.[1].dismiss).toBe(false);
|
||||||
expect(favoriteResult).not.toBeNull();
|
await executeControlAction(controls[1], cardAPI);
|
||||||
|
expect(cardAPI.getNotificationManager().setNotification).toHaveBeenCalled();
|
||||||
|
|
||||||
// 2b. Favorite control (failure)
|
// 2b. Favorite control (failure)
|
||||||
|
vi.mocked(cardAPI.getNotificationManager().setNotification).mockClear();
|
||||||
viewItemManager.favorite.mockRejectedValue(new Error('fail'));
|
viewItemManager.favorite.mockRejectedValue(new Error('fail'));
|
||||||
const favoriteFailureResult = await controls?.[1].callback?.();
|
await executeControlAction(controls[1], cardAPI);
|
||||||
expect(favoriteFailureResult).toBeNull();
|
expect(cardAPI.getNotificationManager().setNotification).not.toHaveBeenCalled();
|
||||||
|
|
||||||
// 3. Download control
|
// 3. Download control
|
||||||
expect(controls?.[2].title).toBe('Download media');
|
expect(controls?.[2].tooltip).toBe('Download media');
|
||||||
const downloadResult = await controls?.[2].callback?.();
|
expect(controls?.[2].dismiss).toBe(true);
|
||||||
expect(downloadResult).toBeNull();
|
vi.mocked(downloadMedia).mockResolvedValue(true);
|
||||||
|
await executeControlAction(controls[2], cardAPI);
|
||||||
|
expect(downloadMedia).toHaveBeenCalledWith(item, viewItemManager);
|
||||||
|
|
||||||
// 4. Timeline control
|
// 4. Timeline control
|
||||||
expect(controls?.[3].title).toBe('See media in timeline');
|
expect(controls?.[3].tooltip).toBe('See media in timeline');
|
||||||
const timelineResult = await controls?.[3].callback?.();
|
expect(controls?.[3].dismiss).toBe(true);
|
||||||
expect(timelineResult).toBeNull();
|
await executeControlAction(controls[3], cardAPI);
|
||||||
|
expect(navigateToTimeline).toHaveBeenCalledWith(item, viewManagerEpoch);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message with controls for already reviewed/favorited items', () => {
|
it('should get notification with controls for already reviewed/favorited items', () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
mediaType: ViewMediaType.Review,
|
mediaType: ViewMediaType.Review,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
@@ -444,18 +472,18 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage(context);
|
const notification = controller.getNotification(context);
|
||||||
const controls = message.controls;
|
const controls = notification.controls;
|
||||||
expect(controls).toHaveLength(2);
|
expect(controls).toHaveLength(2);
|
||||||
|
|
||||||
expect(controls?.[0].title).toBe('Mark as unreviewed');
|
expect(controls?.[0].tooltip).toBe('Mark as unreviewed');
|
||||||
expect(controls?.[0].icon).toEqual({ icon: 'mdi:check-circle' });
|
expect(controls?.[0].icon).toBe('mdi:check-circle');
|
||||||
|
|
||||||
expect(controls?.[1].emphasis).toBe('medium');
|
expect(controls?.[1].severity).toBe('medium');
|
||||||
expect(controls?.[1].icon).toEqual({ icon: 'mdi:star' });
|
expect(controls?.[1].icon).toBe('mdi:star');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message with controls when item has no ID', () => {
|
it('should get notification with controls when item has no ID', () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
id: null,
|
id: null,
|
||||||
});
|
});
|
||||||
@@ -469,11 +497,11 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage(context);
|
const notification = controller.getNotification(context);
|
||||||
expect(message.controls).toHaveLength(0);
|
expect(notification.controls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get message with controls when context has no capabilities', () => {
|
it('should get notification with controls when context has no capabilities', () => {
|
||||||
const item = new TestViewMedia({
|
const item = new TestViewMedia({
|
||||||
id: 'id',
|
id: 'id',
|
||||||
});
|
});
|
||||||
@@ -482,8 +510,8 @@ describe('MediaDetailsController', () => {
|
|||||||
const controller = new MediaDetailsController();
|
const controller = new MediaDetailsController();
|
||||||
controller.calculate(null, item);
|
controller.calculate(null, item);
|
||||||
|
|
||||||
const message = controller.getMessage(context);
|
const notification = controller.getNotification(context);
|
||||||
expect(message.controls).toHaveLength(0);
|
expect(notification.controls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get empty controls when item is null', () => {
|
it('should get empty controls when item is null', () => {
|
||||||
@@ -492,7 +520,7 @@ describe('MediaDetailsController', () => {
|
|||||||
// Use cast to unknown first to avoid any-related lint errors.
|
// Use cast to unknown first to avoid any-related lint errors.
|
||||||
const controls = (
|
const controls = (
|
||||||
controller as unknown as {
|
controller as unknown as {
|
||||||
_getControls: (context: OverlayControlsContext) => OverlayMessageControl[];
|
_getControls: (context: NotificationControlsContext) => NotificationControl[];
|
||||||
}
|
}
|
||||||
)._getControls({});
|
)._getControls({});
|
||||||
expect(controls).toEqual([]);
|
expect(controls).toEqual([]);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { iconSchema } from '../../../../src/config/schema/common/icon';
|
||||||
|
|
||||||
|
describe('iconSchema', () => {
|
||||||
|
it('should parse icon with all fields', () => {
|
||||||
|
expect(
|
||||||
|
iconSchema.parse({
|
||||||
|
icon: 'mdi:star',
|
||||||
|
entity: 'light.office',
|
||||||
|
stateColor: true,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
icon: 'mdi:star',
|
||||||
|
entity: 'light.office',
|
||||||
|
stateColor: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse empty object', () => {
|
||||||
|
expect(iconSchema.parse({})).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -441,6 +441,10 @@ describe('config defaults', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
priority: 50,
|
priority: 50,
|
||||||
},
|
},
|
||||||
|
upgrade: {
|
||||||
|
enabled: true,
|
||||||
|
priority: 50,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
popup_seconds: 3,
|
popup_seconds: 3,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
@@ -1066,6 +1070,30 @@ describe('config defaults', () => {
|
|||||||
action: 'custom:advanced-camera-card-action',
|
action: 'custom:advanced-camera-card-action',
|
||||||
advanced_camera_card_action: 'mute',
|
advanced_camera_card_action: 'mute',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'custom:advanced-camera-card-action',
|
||||||
|
advanced_camera_card_action: 'notification',
|
||||||
|
notification: {
|
||||||
|
heading: {
|
||||||
|
text: 'Attention',
|
||||||
|
icon: 'mdi:alert',
|
||||||
|
severity: 'high',
|
||||||
|
},
|
||||||
|
text: 'Something happened.',
|
||||||
|
details: [{ text: 'Detail 1', icon: 'mdi:info' }],
|
||||||
|
controls: [
|
||||||
|
{
|
||||||
|
icon: 'mdi:check',
|
||||||
|
tooltip: 'Acknowledge',
|
||||||
|
dismiss: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi:eye',
|
||||||
|
dismiss: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'custom:advanced-camera-card-action',
|
action: 'custom:advanced-camera-card-action',
|
||||||
advanced_camera_card_action: 'pause',
|
advanced_camera_card_action: 'pause',
|
||||||
|
|||||||
+2
-2
@@ -45,7 +45,7 @@ import { MediaLoadedInfoManager } from '../src/card-controller/media-info-manage
|
|||||||
import { MediaPlayerManager } from '../src/card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../src/card-controller/media-player-manager';
|
||||||
import { MessageManager } from '../src/card-controller/message-manager';
|
import { MessageManager } from '../src/card-controller/message-manager';
|
||||||
import { MicrophoneManager } from '../src/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../src/card-controller/microphone-manager';
|
||||||
import { OverlayMessageManager } from '../src/card-controller/overlay-message-manager';
|
import { NotificationManager } from '../src/card-controller/notification-manager';
|
||||||
import { PIPManager } from '../src/card-controller/pip-manager';
|
import { PIPManager } from '../src/card-controller/pip-manager';
|
||||||
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
||||||
import { StatusBarItemManager } from '../src/card-controller/status-bar-item-manager';
|
import { StatusBarItemManager } from '../src/card-controller/status-bar-item-manager';
|
||||||
@@ -678,7 +678,7 @@ export const createCardAPI = (): CardController => {
|
|||||||
api.getMediaPlayerManager.mockReturnValue(mock<MediaPlayerManager>());
|
api.getMediaPlayerManager.mockReturnValue(mock<MediaPlayerManager>());
|
||||||
api.getMessageManager.mockReturnValue(mock<MessageManager>());
|
api.getMessageManager.mockReturnValue(mock<MessageManager>());
|
||||||
api.getMicrophoneManager.mockReturnValue(mock<MicrophoneManager>());
|
api.getMicrophoneManager.mockReturnValue(mock<MicrophoneManager>());
|
||||||
api.getOverlayMessageManager.mockReturnValue(mock<OverlayMessageManager>());
|
api.getNotificationManager.mockReturnValue(mock<NotificationManager>());
|
||||||
api.getPIPManager.mockReturnValue(mock<PIPManager>());
|
api.getPIPManager.mockReturnValue(mock<PIPManager>());
|
||||||
api.getQueryStringManager.mockReturnValue(mock<QueryStringManager>());
|
api.getQueryStringManager.mockReturnValue(mock<QueryStringManager>());
|
||||||
api.getStatusBarItemManager.mockReturnValue(mock<StatusBarItemManager>());
|
api.getStatusBarItemManager.mockReturnValue(mock<StatusBarItemManager>());
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
createInternalCallbackAction,
|
createInternalCallbackAction,
|
||||||
createLogAction,
|
createLogAction,
|
||||||
createMediaPlayerAction,
|
createMediaPlayerAction,
|
||||||
|
createNotificationAction,
|
||||||
createPerformAction,
|
createPerformAction,
|
||||||
createPTZAction,
|
createPTZAction,
|
||||||
createPTZControlsAction,
|
createPTZControlsAction,
|
||||||
@@ -357,6 +358,27 @@ describe('createSetReviewAction', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createNotificationAction', () => {
|
||||||
|
it('should create notification action', () => {
|
||||||
|
const notification = { text: 'test' };
|
||||||
|
expect(createNotificationAction(notification)).toEqual({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
advanced_camera_card_action: 'notification',
|
||||||
|
notification,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create notification action with cardID', () => {
|
||||||
|
const notification = { text: 'test' };
|
||||||
|
expect(createNotificationAction(notification, { cardID: 'card_id' })).toEqual({
|
||||||
|
action: 'fire-dom-event',
|
||||||
|
advanced_camera_card_action: 'notification',
|
||||||
|
notification,
|
||||||
|
card_id: 'card_id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getActionConfigGivenAction', () => {
|
describe('getActionConfigGivenAction', () => {
|
||||||
const action = createViewAction('clips');
|
const action = createViewAction('clips');
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import { dispatchDismissNotificationEvent } from '../../src/utils/notification';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('notification utils', () => {
|
||||||
|
it('should dispatch dismiss notification event', () => {
|
||||||
|
const element = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
element.addEventListener('advanced-camera-card:notification:dismiss', handler);
|
||||||
|
|
||||||
|
dispatchDismissNotificationEvent(element);
|
||||||
|
|
||||||
|
expect(handler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
dispatchDismissOverlayMessageEvent,
|
|
||||||
dispatchShowOverlayMessageEvent,
|
|
||||||
} from '../../src/utils/overlay-message';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('overlay-message utils', () => {
|
|
||||||
it('should dispatch show overlay message event', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const message = { text: 'test' };
|
|
||||||
const handler = vi.fn();
|
|
||||||
element.addEventListener('advanced-camera-card:overlay-message:show', handler);
|
|
||||||
|
|
||||||
dispatchShowOverlayMessageEvent(element, message);
|
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalled();
|
|
||||||
const event = handler.mock.calls[0][0];
|
|
||||||
expect(event.detail).toBe(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch dismiss overlay message event', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const handler = vi.fn();
|
|
||||||
element.addEventListener('advanced-camera-card:overlay-message:dismiss', handler);
|
|
||||||
|
|
||||||
dispatchDismissOverlayMessageEvent(element);
|
|
||||||
|
|
||||||
expect(handler).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user