From 24613e7ed9fd78755d8c56ff3e19874ac95615c9 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 11 Mar 2023 15:38:54 -0800 Subject: [PATCH] Add support for expanding the card without fullscreen. --- README.md | 36 +++++++++++++---- package.json | 1 + src/card-condition.ts | 5 +++ src/card.ts | 64 ++++++++++++++++++++++++++++--- src/components/carousel.ts | 2 +- src/editor.ts | 1 + src/localize/languages/en.json | 1 + src/localize/languages/it.json | 1 + src/localize/languages/pt-BR.json | 1 + src/scss/card.scss | 29 ++++++++++++++ src/types.ts | 20 ++++++---- yarn.lock | 17 ++++++++ 12 files changed, 157 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 50f767a3..1d2aba59 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,7 @@ menu: | `download` | :white_check_mark: | The `download` menu button: allow direct download of the media being displayed.| | `camera_ui` | :white_check_mark: | The `camera_ui` menu button: brings the user to a context-appropriate page on the UI of their camera engine (e.g. the Frigate camera homepage). Will only appear if the camera engine supports a camera UI (e.g. if `frigate.url` option is set for `frigate` engine users).| | `fullscreen` | :white_check_mark: | The `fullscreen` menu button: expand the card to consume the fullscreen. | +| `expand` | :white_check_mark: | The `expand` menu button: expand the card into a popup/dialog. | | `timeline` | :white_check_mark: | The `timeline` menu button: show the event timeline. | | `media_player` | :white_check_mark: | The `media_player` menu button: sends the visible media to a remote media player. Supports Frigate clips, snapshots and live camera (only for cameras that specify a `camera_entity` and only using the default HA stream (equivalent to the `ha` live provider). `jsmpeg` or `webrtc-card` are not supported, although live can still be played as long as `camera_entity` is specified. In the player list, a `tap` will send the media to the player, a `hold` will stop the media on the player. | @@ -947,6 +948,7 @@ All variables listed are under a `conditions:` section. | `view` | A list of [views](#views) in which this condition is satified (e.g. `clips`) | | `camera` | A list of camera ids in which this condition is satisfied. See [camera IDs](#camera-ids).| | `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.| +| `expand` | If `true` the condition is satisfied if the card is in expanded mode (in a dialog/popup). If `false` the condition is satisfied if the card is **NOT** in expanded mode (in a dialog/popup).| | `state` | A list of state conditions to compare with Home Assistant state. See below. | | `media_loaded` | 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).| @@ -1090,7 +1092,7 @@ Parameters for the `custom:frigate-card-ptz` element: | Parameter | Description | | - | - | | `action` | Must be `custom:frigate-card-action`. | -| `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`.| +| `frigate_card_action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `recording`, `recordings`, `snapshot`, `snapshots`, `download`, `timeline`, `camera_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`, `live_substream_select`, `expand_toggle`.| ##### Command descriptions @@ -1104,7 +1106,8 @@ Parameters for the `custom:frigate-card-ptz` element: |`camera_select`|Select a given camera. Takes a single additional `camera` parameter with the [camera ID](#camera-ids) of the camera to select. Respects the value of `view.camera_select` to choose the appropriate view on the new camera.| |`menu_toggle` | Show/hide the menu (for the `hidden` mode style). | |`media_player`| Perform a media player action. Takes a `media_player` parameter with the entity ID of the media_player on which to perform the action, and a `media_player_action` parameter which should be either `play` or `stop` to play or stop the media in question. | - +|`live_substream_select`| Perform a media player action. Takes a `camera` parameter with the [camera ID](#camera-ids) of the substream camera. | +|`expand_toggle`| Expand the card into a dialog/popup. | @@ -1595,6 +1598,11 @@ menu: enabled: true alignment: matching icon: mdi:fullscreen + expand: + priority: 50 + enabled: true + alignment: matching + icon: mdi:arrow-expand-all media_player: priority: 50 enabled: false @@ -2838,8 +2846,6 @@ overrides: - -
Expand: Change the menu position based on HA state @@ -2855,9 +2861,6 @@ overrides: ```
- - -
Expand: Change the default view based on HA state @@ -2888,6 +2891,25 @@ overrides: ```
+ +
+ Expand: Change the menu style in expanded mode + +This example changes the menu style to `overlay` in expanded mode in order to take +advantage of the extra horizontal space of the dialog/popup. + +```yaml +menu: + style: hidden +overrides: + - conditions: + expand: true + overrides: + menu: + style: overlay +``` +
+ ### Refreshing a static image
diff --git a/package.json b/package.json index 8b033069..690ca3fd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "vis-data": "^7.1.4", "vis-timeline": "^7.7.0", "vis-util": "^5.0.2", + "web-dialog": "^0.0.11", "xss": "^1.0.14", "zod": "^3.20.6" }, diff --git a/src/card-condition.ts b/src/card-condition.ts index cdbd6113..59dbf0d0 100644 --- a/src/card-condition.ts +++ b/src/card-condition.ts @@ -12,6 +12,7 @@ import { copyConfig } from './config-mgmt'; export interface ConditionState { view?: string; fullscreen?: boolean; + expand?: boolean; camera?: string; state?: HassEntities; media_loaded?: boolean; @@ -37,6 +38,10 @@ function evaluateCondition( result &&= state.fullscreen !== undefined && condition.fullscreen == state.fullscreen; } + if (condition?.expand !== undefined) { + result &&= + state.expand !== undefined && condition.expand == state.expand; + } if (condition?.camera?.length) { result &&= !!state.camera && condition.camera.includes(state.camera); } diff --git a/src/card.ts b/src/card.ts index 23f712f4..807a1fdc 100644 --- a/src/card.ts +++ b/src/card.ts @@ -91,6 +91,7 @@ import cloneDeep from 'lodash-es/cloneDeep'; import isEqual from 'lodash-es/isEqual'; import merge from 'lodash-es/merge'; import { FrigateCardInitializer } from './utils/initializer.js'; +import 'web-dialog'; /** A note on media callbacks: * @@ -175,6 +176,9 @@ class FrigateCard extends LitElement { @property({ attribute: 'panel', type: Boolean, reflect: true }) protected _panel = false; + @state() + protected _expand?: boolean = false; + protected _conditionState?: ConditionState; protected _refMenu: Ref = createRef(); @@ -294,6 +298,7 @@ class FrigateCard extends LitElement { this._conditionState = { view: this._view?.view, fullscreen: screenfull.isEnabled && screenfull.isFullscreen, + expand: this._expand, camera: this._view?.camera, media_loaded: !!this._currentMediaLoadedInfo, ...(this._conditionManager?.hasHAStateConditions && { @@ -622,6 +627,17 @@ class FrigateCard extends LitElement { }); } + buttons.push({ + icon: this._expand ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all', + ...this._getConfig().menu.buttons.expand, + type: 'custom:frigate-card-menu-icon', + title: localize('config.menu.buttons.expand'), + tap_action: createFrigateCardCustomAction( + 'expand_toggle', + ) as FrigateCardCustomAction, + style: this._expand ? this._getEmphasizedStyle() : {}, + }); + if ( this._mediaPlayers?.length && (this._view?.isViewerView() || @@ -1510,6 +1526,9 @@ class FrigateCard extends LitElement { case 'diagnostics': this._diagnostics(); break; + case 'expand_toggle': + this._setExpand(!this._expand); + break; default: console.warn(`Frigate card received unknown card action: ${action}`); } @@ -1784,6 +1803,13 @@ class FrigateCard extends LitElement { this._lastValidMediaLoadedInfo = this._currentMediaLoadedInfo = mediaLoadedInfo; + // When a new media loads, set the aspect ratio for when the card is + // expanded/popped-up. + this.style.setProperty( + '--frigate-card-expand-aspect-ratio', + this._getAspectRatioStyle(), + ); + // An update may be required to draw elements. this._generateConditionState(); this.requestUpdate(); @@ -1869,12 +1895,20 @@ class FrigateCard extends LitElement { * @returns A padding percentage. */ protected _getAspectRatioStyle(): string { - if (!this._isAspectRatioEnforced()) { + // In expanded mode we must always set the aspect ratio since there are no + // constraints on the size. + + if (!this._expand && !this._isAspectRatioEnforced()) { return 'auto'; } const aspectRatioMode = this._getConfig().dimensions.aspect_ratio_mode; - if (aspectRatioMode == 'dynamic' && this._lastValidMediaLoadedInfo) { + + if ( + this._lastValidMediaLoadedInfo && + (aspectRatioMode === 'dynamic' || + (this._expand && aspectRatioMode === 'unconstrained')) + ) { return `${this._lastValidMediaLoadedInfo.width} / ${this._lastValidMediaLoadedInfo.height}`; } @@ -1914,6 +1948,27 @@ class FrigateCard extends LitElement { return { ...this._getConfig().view.actions, ...specificActions }; } + protected _setExpand(expand: boolean): void { + this._expand = expand; + this._generateConditionState(); + } + + protected _renderInDialogIfNecessary(contents: TemplateResult): TemplateResult | void { + if (this._expand) { + return html` { + this._setExpand(false); + }} + > + ${contents} + `; + } else { + return contents; + } + } + /** * Master render method for the card. */ @@ -1947,8 +2002,7 @@ class FrigateCard extends LitElement { // Caution: Keep the main div and the menu next to one another in order to // ensure the hover menu styling continues to work. - - return html` ` : ``} - `; + `); } /** diff --git a/src/components/carousel.ts b/src/components/carousel.ts index a7ce292d..351bf339 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -206,7 +206,7 @@ export class FrigateCardCarousel extends LitElement { nodes, { axis: this.direction == 'horizontal' ? 'x' : 'y', - speed: 20, + speed: 30, startIndex: this.selected, ...this.carouselOptions, }, diff --git a/src/editor.ts b/src/editor.ts index 82be6d77..4e41dd08 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1657,6 +1657,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderMenuButton('download')} ${this._renderMenuButton('camera_ui')} ${this._renderMenuButton('fullscreen')} + ${this._renderMenuButton('expand')} ${this._renderMenuButton('timeline')} ${this._renderMenuButton('media_player')} diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 45e470ec..c7db334e 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -240,6 +240,7 @@ "clips": "Clips", "download": "Download", "enabled": "Button enabled", + "expand": "Expand", "frigate": "Frigate menu / Default view", "fullscreen": "Fullscreen", "icon": "Icon", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index f1fb00f2..17cd1def 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -239,6 +239,7 @@ "clips": "Clip", "download": "Download", "enabled": "Pulsante abilitato", + "expand": "", "frigate": "Frigate menu / Visualizzazione predefinita", "fullscreen": "A schermo intero", "icon": "Icona", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index dbfd2241..84617184 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -239,6 +239,7 @@ "clips": "Clipes", "download": "Baixe a mídia do evento", "enabled": "Botão ativado", + "expand": "", "frigate": "Frigate menu / Visualização padrão", "fullscreen": "Tela cheia", "icon": "Ícone", diff --git a/src/scss/card.scss b/src/scss/card.scss index c4c2736c..8c67e287 100644 --- a/src/scss/card.scss +++ b/src/scss/card.scss @@ -85,6 +85,8 @@ ha-card { flex-direction: column; margin: auto; + border: 0px; + // Some elements (such as submenus) may need to extend beyond the card boundary. overflow: visible; width: 100%; @@ -151,3 +153,30 @@ frigate-card-live.hidden { :host(:-webkit-full-screen) frigate-card-menu { @include fullscreen-no-rounded-corners; } + +/*************** + * Expanded mode + ***************/ + +web-dialog { + --dialog-padding: 0px; + --dialog-container-padding: 0px; + + // The standard HA header is 56 pixels wide, so that much off the top (header) + // and bottom (to maintain center), before doing the calculation of + // max-height. This matters on small mobile devices in landscape orientation. + --dialog-max-height: calc( ( 100vh - (2 * 56px) ) * 0.85 ); + --dialog-max-width: 85vw; + + --dialog-width: none; + --dialog-height: none; + + // Allow submenus to flow outside the edge of the dialog. + --dialog-overflow-x: visible; + --dialog-overflow-y: visible; +} + +web-dialog::part(dialog) { + aspect-ratio: var(--frigate-card-expand-aspect-ratio); + height: 100%; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 89ecbc61..2c8625b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -208,21 +208,22 @@ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({ }); const FRIGATE_CARD_GENERAL_ACTIONS = [ - 'default', + 'camera_ui', 'clip', 'clips', + 'default', + 'diagnostics', + 'expand_toggle', + 'download', + 'fullscreen', 'image', 'live', + 'menu_toggle', + 'recording', + 'recordings', 'snapshot', 'snapshots', 'timeline', - 'download', - 'camera_ui', - 'fullscreen', - 'menu_toggle', - 'diagnostics', - 'recording', - 'recordings', ] as const; const FRIGATE_CARD_ACTIONS = [ ...FRIGATE_CARD_GENERAL_ACTIONS, @@ -580,6 +581,7 @@ export type MenuItem = MenuIcon | MenuStateIcon | MenuSubmenu | MenuSubmenuSelec const frigateCardConditionSchema = z.object({ view: z.string().array().optional(), fullscreen: z.boolean().optional(), + expand: z.boolean().optional(), camera: z.string().array().optional(), media_loaded: z.boolean().optional(), state: stateConditions.optional(), @@ -972,6 +974,7 @@ const menuConfigDefault = { download: visibleButtonDefault, camera_ui: visibleButtonDefault, fullscreen: visibleButtonDefault, + expand: hiddenButtonDefault, media_player: visibleButtonDefault, recordings: hiddenButtonDefault, }, @@ -1005,6 +1008,7 @@ const menuConfigSchema = z download: visibleButtonSchema.default(menuConfigDefault.buttons.download), camera_ui: visibleButtonSchema.default(menuConfigDefault.buttons.camera_ui), fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen), + expand: hiddenButtonSchema.default(menuConfigDefault.buttons.expand), media_player: visibleButtonSchema.default( menuConfigDefault.buttons.media_player, ), diff --git a/yarn.lock b/yarn.lock index 612741d1..77bbc94c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 6 cacheKey: 8 +"@a11y/focus-trap@npm:^1.0.5": + version: 1.0.5 + resolution: "@a11y/focus-trap@npm:1.0.5" + checksum: aa5dbdcbfa0ce2de0a9778c1696a67c25ce28e06126079bb8c302a97fbffe620608a4fc0038a3ee0c0cfbbd4dc7fdafe029353e7aa06b0e5fdf9da69b0008b6b + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -2455,6 +2462,7 @@ __metadata: vis-data: ^7.1.4 vis-timeline: ^7.7.0 vis-util: ^5.0.2 + web-dialog: ^0.0.11 xss: ^1.0.14 zod: ^3.20.6 languageName: unknown @@ -5318,6 +5326,15 @@ __metadata: languageName: node linkType: hard +"web-dialog@npm:^0.0.11": + version: 0.0.11 + resolution: "web-dialog@npm:0.0.11" + dependencies: + "@a11y/focus-trap": ^1.0.5 + checksum: bafe9ed971d30afb9db30ea23193ab941f236a8e8295b084b76463a4181310ccbc4c493d6317fdb2aff6dbb4d577c32f5e47f3750d2237e0396029ff47715613 + languageName: node + linkType: hard + "wheel-gestures@npm:^2.2.5": version: 2.2.5 resolution: "wheel-gestures@npm:2.2.5"