Merge pull request #866 from dermotduffy/pip
Add media query support (conditions based on device properties such as orientation or viewport width)
This commit is contained in:
@@ -874,6 +874,7 @@ All variables listed are under a `conditions:` section.
|
|||||||
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.|
|
| `fullscreen` | If `true` the condition is satisfied if the card is in fullscreen mode. If `false` the condition is satisfied if the card is **NOT** in fullscreen mode.|
|
||||||
| `state` | A list of state conditions to compare with Home Assistant state. See below. |
|
| `state` | A list of state conditions to compare with Home Assistant state. See below. |
|
||||||
| `mediaLoaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).|
|
| `mediaLoaded` | If `true` the condition is satisfied if there is media load**ED** (not load**ING**) in the card (e.g. a clip, snapshot or live view). This may be used to hide controls during media loading or when a message (not media) is being displayed. Note that if `true` this condition will never be satisfied for views that do not themselves load media directly (e.g. gallery).|
|
||||||
|
| `media_query` | Any valid [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) string. Media queries must start and end with parentheses. This may be used to alter card configuration based on device/media properties (e.g. viewport width, orientation). Please note that `width` and `height` refer to the entire viewport not just the card. See the [media query example](#media-query-example).|
|
||||||
|
|
||||||
See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
|
See the [example below](#frigate-card-conditional-example) for a real-world example of how these conditions can be used.
|
||||||
|
|
||||||
@@ -2960,8 +2961,55 @@ overrides:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<a name="media-query-example"></a>
|
||||||
|
|
||||||
<a name="card-updates"></a>
|
### Using Media Query conditions
|
||||||
|
|
||||||
|
Alter the card configuration based on device or viewport properties.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Hide menu & controls when viewport width <= 300 (e.g. PIP mode)</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:frigate-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.back_yard
|
||||||
|
- camera_entity: camera.sitting_room
|
||||||
|
overrides:
|
||||||
|
- conditions:
|
||||||
|
media_query: '(max-width: 300px)'
|
||||||
|
overrides:
|
||||||
|
menu:
|
||||||
|
style: none
|
||||||
|
live:
|
||||||
|
controls:
|
||||||
|
next_previous:
|
||||||
|
style: none
|
||||||
|
thumbnails:
|
||||||
|
mode: none
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Change menu position when orientation changes</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: custom:frigate-card
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.back_yard
|
||||||
|
- camera_entity: camera.sitting_room
|
||||||
|
menu:
|
||||||
|
style: overlay
|
||||||
|
overrides:
|
||||||
|
- conditions:
|
||||||
|
media_query: '(orientation: landscape)'
|
||||||
|
overrides:
|
||||||
|
menu:
|
||||||
|
position: left
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<a name="media-layout-examples"></a>
|
||||||
|
|
||||||
## Card Refreshes
|
## Card Refreshes
|
||||||
|
|
||||||
|
|||||||
+85
-1
@@ -1,5 +1,7 @@
|
|||||||
import type {
|
import {
|
||||||
FrigateCardCondition,
|
FrigateCardCondition,
|
||||||
|
FrigateCardConfig,
|
||||||
|
frigateConditionalSchema,
|
||||||
OverrideConfigurationKey,
|
OverrideConfigurationKey,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -54,6 +56,9 @@ export function evaluateCondition(
|
|||||||
result &&=
|
result &&=
|
||||||
state.mediaLoaded !== undefined && condition.mediaLoaded == state.mediaLoaded;
|
state.mediaLoaded !== undefined && condition.mediaLoaded == state.mediaLoaded;
|
||||||
}
|
}
|
||||||
|
if (condition?.media_query) {
|
||||||
|
result &&= window.matchMedia(condition.media_query).matches;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,3 +143,82 @@ export function getOverridesByKey(
|
|||||||
})) ?? []
|
})) ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CardConditionManager {
|
||||||
|
// Whether or not to include HA state in ConditionState. Doing so increases
|
||||||
|
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
||||||
|
// the configuration needs to consume it.
|
||||||
|
protected _hasHAStateConditions = false;
|
||||||
|
|
||||||
|
protected _callback: () => void;
|
||||||
|
protected _mediaQueries: MediaQueryList[] = [];
|
||||||
|
protected _boundTriggerChange = this._triggerChange.bind(this);
|
||||||
|
|
||||||
|
constructor(config: FrigateCardConfig, callback: () => void) {
|
||||||
|
this._initConditions(config);
|
||||||
|
this._callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the object.
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
this._mediaQueries.forEach((mql) =>
|
||||||
|
mql.removeEventListener('change', this._boundTriggerChange),
|
||||||
|
);
|
||||||
|
this._mediaQueries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the conditions have state conditions.
|
||||||
|
*/
|
||||||
|
get hasHAStateConditions(): boolean {
|
||||||
|
return this._hasHAStateConditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger the callback.
|
||||||
|
* @param _ Ignored parameter.
|
||||||
|
*/
|
||||||
|
protected _triggerChange(_): void {
|
||||||
|
this._callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init the conditions.
|
||||||
|
* @param config The card configuration.
|
||||||
|
*/
|
||||||
|
protected _initConditions(config: FrigateCardConfig): void {
|
||||||
|
const getAllConditions = (config: FrigateCardConfig): FrigateCardCondition[] => {
|
||||||
|
const conditions: FrigateCardCondition[] = [];
|
||||||
|
config.overrides?.forEach((override) => conditions.push(override.conditions));
|
||||||
|
|
||||||
|
// Element conditions can be arbitrarily nested underneath conditionals and
|
||||||
|
// custom elements that this card may not known. Here we recursively parse
|
||||||
|
// down the elements tree, parsing as we go to find valid conditions.
|
||||||
|
const getElementsConditions = (data: unknown): void => {
|
||||||
|
const parseResult = frigateConditionalSchema.safeParse(data);
|
||||||
|
if (parseResult.success) {
|
||||||
|
conditions.push(parseResult.data.conditions);
|
||||||
|
parseResult.data.elements?.forEach(getElementsConditions);
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
config.elements?.forEach(getElementsConditions);
|
||||||
|
return conditions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditions = getAllConditions(config);
|
||||||
|
this._hasHAStateConditions = conditions.some(
|
||||||
|
(condition) => !!condition.state?.length,
|
||||||
|
);
|
||||||
|
conditions.forEach((condition) => {
|
||||||
|
if (condition.media_query) {
|
||||||
|
const mql = window.matchMedia(condition.media_query);
|
||||||
|
mql.addEventListener('change', this._boundTriggerChange);
|
||||||
|
this._mediaQueries.push(mql);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-24
@@ -17,6 +17,7 @@ import screenfull from 'screenfull';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { actionHandler } from './action-handler-directive.js';
|
import { actionHandler } from './action-handler-directive.js';
|
||||||
import {
|
import {
|
||||||
|
CardConditionManager,
|
||||||
ConditionState,
|
ConditionState,
|
||||||
conditionStateRequestHandler,
|
conditionStateRequestHandler,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
@@ -212,10 +213,7 @@ export class FrigateCard extends LitElement {
|
|||||||
protected _triggers: Map<string, Date> = new Map();
|
protected _triggers: Map<string, Date> = new Map();
|
||||||
protected _untriggerTimerID: number | null = null;
|
protected _untriggerTimerID: number | null = null;
|
||||||
|
|
||||||
// Whether or not to include HA state in ConditionState. Doing so increases
|
protected _conditionManager: CardConditionManager | null = null;
|
||||||
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
|
||||||
// the configuration is likely to consume it.
|
|
||||||
protected _needHAStateInConditionState = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the Home Assistant object.
|
* Set the Home Assistant object.
|
||||||
@@ -238,7 +236,7 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._needHAStateInConditionState) {
|
if (this._conditionManager?.hasHAStateConditions) {
|
||||||
// HA entity state is part of the condition state.
|
// HA entity state is part of the condition state.
|
||||||
this._generateConditionState();
|
this._generateConditionState();
|
||||||
}
|
}
|
||||||
@@ -288,9 +286,9 @@ export class FrigateCard extends LitElement {
|
|||||||
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
fullscreen: screenfull.isEnabled && screenfull.isFullscreen,
|
||||||
camera: this._view?.camera,
|
camera: this._view?.camera,
|
||||||
mediaLoaded: !!this._currentMediaLoadedInfo,
|
mediaLoaded: !!this._currentMediaLoadedInfo,
|
||||||
...(this._needHAStateInConditionState && {
|
...(this._conditionManager?.hasHAStateConditions && {
|
||||||
state: this._hass?.states,
|
state: this._hass?.states,
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the components that need the new condition state. Passed directly
|
// Update the components that need the new condition state. Passed directly
|
||||||
@@ -822,7 +820,9 @@ export class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
// Load all cameras in parallel, but remember the order they were provided
|
// Load all cameras in parallel, but remember the order they were provided
|
||||||
// (they must be added to the cameraMap in this same order).
|
// (they must be added to the cameraMap in this same order).
|
||||||
await Promise.all(configCameras.map((configCamera, index) => addCameraConfig(configCamera, index)))
|
await Promise.all(
|
||||||
|
configCameras.map((configCamera, index) => addCameraConfig(configCamera, index)),
|
||||||
|
);
|
||||||
|
|
||||||
loadedCameras.forEach((loadedCamera: CameraConfig) => {
|
loadedCameras.forEach((loadedCamera: CameraConfig) => {
|
||||||
const id = getCameraID(loadedCamera);
|
const id = getCameraID(loadedCamera);
|
||||||
@@ -990,24 +990,16 @@ export class FrigateCard extends LitElement {
|
|||||||
this._cameras = undefined;
|
this._cameras = undefined;
|
||||||
this._view = undefined;
|
this._view = undefined;
|
||||||
this._message = null;
|
this._message = null;
|
||||||
|
|
||||||
|
this._conditionManager?.destroy();
|
||||||
|
this._conditionManager = new CardConditionManager(
|
||||||
|
config,
|
||||||
|
this._generateConditionState.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
this._generateConditionState();
|
this._generateConditionState();
|
||||||
this._setLightOrDarkMode();
|
this._setLightOrDarkMode();
|
||||||
this._untrigger();
|
this._untrigger();
|
||||||
|
|
||||||
const hasStateCondition = (): boolean => {
|
|
||||||
for (const override of this._config.overrides ?? []) {
|
|
||||||
if (override.conditions.state?.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For elements there can be arbitrary custom elements that not related to
|
|
||||||
// this card so we cannot easily determine if state is used further down
|
|
||||||
// the chain by a custom:frigate-card-conditional element. Instead, include state
|
|
||||||
// if elements are configured at all.
|
|
||||||
return !!this._config.elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._needHAStateInConditionState = hasStateCondition();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1970,7 +1962,7 @@ export class FrigateCard extends LitElement {
|
|||||||
this._changeView({ resetMessage: false });
|
this._changeView({ resetMessage: false });
|
||||||
return this._render();
|
return this._render();
|
||||||
})(),
|
})(),
|
||||||
renderProgressIndicator({cardWideConfig: this._cardWideConfig}),
|
renderProgressIndicator({ cardWideConfig: this._cardWideConfig }),
|
||||||
)
|
)
|
||||||
: // Always want to call render even if there's a message, to
|
: // Always want to call render even if there's a message, to
|
||||||
// ensure live preload is always present (even if not displayed).
|
// ensure live preload is always present (even if not displayed).
|
||||||
|
|||||||
+2
-1
@@ -495,10 +495,11 @@ const frigateCardConditionSchema = z.object({
|
|||||||
camera: z.string().array().optional(),
|
camera: z.string().array().optional(),
|
||||||
mediaLoaded: z.boolean().optional(),
|
mediaLoaded: z.boolean().optional(),
|
||||||
state: stateConditions.optional(),
|
state: stateConditions.optional(),
|
||||||
|
media_query: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
export type FrigateCardCondition = z.infer<typeof frigateCardConditionSchema>;
|
||||||
|
|
||||||
const frigateConditionalSchema = z.object({
|
export const frigateConditionalSchema = z.object({
|
||||||
type: z.literal('custom:frigate-card-conditional'),
|
type: z.literal('custom:frigate-card-conditional'),
|
||||||
conditions: frigateCardConditionSchema,
|
conditions: frigateCardConditionSchema,
|
||||||
elements: z.lazy(() => pictureElementsSchema),
|
elements: z.lazy(() => pictureElementsSchema),
|
||||||
|
|||||||
Reference in New Issue
Block a user