feat: Add 'call' support to improve 2-way audio experience (#2486)
Draws significant inspiration (and direct styling) from https://github.com/dermotduffy/advanced-camera-card/pull/2447 . Thank you @Maudfer ! BREAKING CHANGE: The microphone condition previously bundled two unrelated signals — whether a two-way-audio session was connected and whether the microphone was muted. Connection state is now its own dedicated call condition, and microphone is reserved purely for mute state. Configs are upgraded automatically (the card rewrites affected conditions under overrides, elements, and automations). If you maintain config by hand, convert as follows: If you only used connected: # Before ```yaml condition: microphone connected: true ``` # After ```yaml condition: call call: true ``` If you used both connected and muted — they must be split into two conditions, since they no longer live together: # Before ```yaml condition: microphone connected: true muted: false ``` # After ```yaml condition: and conditions: - condition: call call: true - condition: microphone muted: false ```
This commit is contained in:
committed by
dermotduffy
parent
bb061a1a55
commit
abcba884e5
@@ -0,0 +1,56 @@
|
||||
@use './z-index.scss' as *;
|
||||
@use './pop-animation.scss' as *;
|
||||
|
||||
:host {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: 16px;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
z-index: $z-index-call-controls;
|
||||
|
||||
--advanced-camera-card-call-controls-button-size: 40px;
|
||||
--ha-icon-button-size: var(--advanced-camera-card-call-controls-button-size);
|
||||
--mdc-icon-size: calc(var(--ha-icon-button-size) / 2);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 7px;
|
||||
border-radius: var(--advanced-camera-card-button-border-radius);
|
||||
background: var(--advanced-camera-card-call-controls-background);
|
||||
box-shadow: var(
|
||||
--advanced-camera-card-box-shadow-override,
|
||||
0 10px 30px rgba(0, 0, 0, 0.25)
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
@include pop-in;
|
||||
}
|
||||
|
||||
.panel.exiting {
|
||||
@include pop-out;
|
||||
|
||||
// The pill lingers in the DOM for the exit animation; stop it taking clicks
|
||||
// so a stale hangup/mute can't fire after the call has already ended.
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-icon-button {
|
||||
color: var(--advanced-camera-card-button-color);
|
||||
background: var(--advanced-camera-card-button-background);
|
||||
border-radius: var(--advanced-camera-card-button-border-radius);
|
||||
|
||||
&.critical {
|
||||
color: var(--advanced-camera-card-call-controls-critical-color);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
@use './z-index.scss' as *;
|
||||
|
||||
:host {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
// Picture elements always render above view content (media, gallery,
|
||||
// thumbnails) regardless of any z-index those surfaces use internally.
|
||||
z-index: $z-index-elements;
|
||||
|
||||
// Don't let elements overflow.
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
+26
-3
@@ -2,13 +2,13 @@
|
||||
|
||||
@keyframes warning-pulse {
|
||||
0% {
|
||||
border: solid 2px var(--trigger-border-color-base);
|
||||
border-color: var(--trigger-border-color-base);
|
||||
}
|
||||
50% {
|
||||
border: solid 2px var(--trigger-border-color);
|
||||
border-color: var(--trigger-border-color);
|
||||
}
|
||||
100% {
|
||||
border: solid 2px var(--trigger-border-color-base);
|
||||
border-color: var(--trigger-border-color-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,34 @@ advanced-camera-card-live-carousel {
|
||||
--advanced-camera-card-trigger-border-color-base,
|
||||
black
|
||||
);
|
||||
|
||||
transition: border-color 0.3s ease-out;
|
||||
}
|
||||
|
||||
// `:host >` matches the carousel only in single display mode (in grid mode it
|
||||
// is nested inside `media-grid`). The single-mode carousel has no border of its
|
||||
// own, so reserve a transparent one so border changes (e.g. trigger) only
|
||||
// recolour. In grid mode the border is supplied (already width-reserved) by
|
||||
// `media-grid`.
|
||||
:host > advanced-camera-card-live-carousel {
|
||||
box-sizing: border-box;
|
||||
border: solid 2px transparent;
|
||||
}
|
||||
|
||||
advanced-camera-card-live-carousel[triggered] {
|
||||
animation: warning-pulse 5s infinite;
|
||||
}
|
||||
// The `:host` prefix raises specificity to (0,2,1), above `media-grid`'s
|
||||
// `::slotted([selected])` border (0,1,1). In grid mode that rule lives in an
|
||||
// inner shadow tree and would otherwise win the cross-tree cascade tie,
|
||||
// leaving the transmitting border the wrong colour. `[triggered]` needs no
|
||||
// such treatment — animated values always beat the regular cascade.
|
||||
:host advanced-camera-card-live-carousel[transmitting] {
|
||||
// `animation: none` cancels any concurrent trigger pulse to ensure
|
||||
// transmitting takes precedence.
|
||||
animation: none;
|
||||
border-color: var(--advanced-camera-card-transmitting-border-color);
|
||||
}
|
||||
advanced-camera-card-live-carousel[selected] {
|
||||
--trigger-border-color-base: var(
|
||||
--advanced-camera-card-trigger-border-color-base,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@use './button.scss';
|
||||
@use 'locked.scss';
|
||||
|
||||
:host {
|
||||
--advanced-camera-card-next-prev-size: 48px;
|
||||
@@ -24,6 +23,17 @@
|
||||
right: var(--advanced-camera-card-right-position);
|
||||
}
|
||||
|
||||
// Dim while locked. Deliberately applied to `.controls` rather than `:host`
|
||||
// (as the shared `locked.scss` does): `opacity` on the host would create a
|
||||
// stacking context that traps `.controls`'s `z-index`, dropping the control
|
||||
// behind the media whenever it is DOM-ordered before it (the left control).
|
||||
// `.controls` is already positioned and z-indexed, so dimming it here leaves
|
||||
// its stacking context intact.
|
||||
:host([locked]) .controls {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.controls.icons {
|
||||
top: calc(50% - (var(--advanced-camera-card-next-prev-size) / 2));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@use './z-index.scss' as *;
|
||||
@use './pop-animation.scss' as *;
|
||||
@use './notification-common.scss';
|
||||
|
||||
:host {
|
||||
@@ -62,45 +63,11 @@
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
// Entry animation (auto-plays on render)
|
||||
animation: slideUp 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
@include pop-in;
|
||||
}
|
||||
|
||||
// Exit animation
|
||||
.notification.exiting {
|
||||
animation: slideDown 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translateY(-6px) scale(1.02);
|
||||
}
|
||||
80% {
|
||||
transform: translateY(3px) scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
@include pop-out;
|
||||
}
|
||||
|
||||
.controls {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Shared "pop" enter/exit animation for overlays.
|
||||
//
|
||||
// `@include pop-in` auto-plays an entrance on render. `@include pop-out` —
|
||||
// typically guarded by an `.exiting` class — plays the matching exit; it is
|
||||
// named `pop-out` so an `animationend` handler can detect exit completion and
|
||||
// unmount the element.
|
||||
|
||||
@mixin pop-in {
|
||||
animation: pop-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||||
}
|
||||
|
||||
@mixin pop-out {
|
||||
animation: pop-out 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes pop-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translateY(-6px) scale(1.02);
|
||||
}
|
||||
80% {
|
||||
transform: translateY(3px) scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,20 @@
|
||||
// elements render as pills/capsules rather than ovals.
|
||||
--advanced-camera-card-button-border-radius: 100vmax;
|
||||
|
||||
/***************
|
||||
* Call controls
|
||||
***************/
|
||||
|
||||
// The background of the call controls.
|
||||
--advanced-camera-card-call-controls-background: var(
|
||||
--advanced-camera-card-control-background-transparent
|
||||
);
|
||||
|
||||
// The color of the call controls end-call (critical) button.
|
||||
--advanced-camera-card-call-controls-critical-color: var(
|
||||
--advanced-camera-card-warning-color
|
||||
);
|
||||
|
||||
/******
|
||||
* Menu
|
||||
******/
|
||||
@@ -234,6 +248,13 @@
|
||||
--advanced-camera-card-trigger-border-color: var(--advanced-camera-card-warning-color);
|
||||
--advanced-camera-card-trigger-border-color-base: unset;
|
||||
|
||||
/**************
|
||||
* Transmitting
|
||||
**************/
|
||||
--advanced-camera-card-transmitting-border-color: var(
|
||||
--advanced-camera-card-warning-color
|
||||
);
|
||||
|
||||
/*****
|
||||
* Grid
|
||||
******/
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
box-sizing: border-box;
|
||||
gap: 2px;
|
||||
|
||||
// Ensure control icons are relative to the thumbnail.
|
||||
// Position control icons relative to the thumbnail, and `isolation: isolate`
|
||||
// to keep their `z-index` scoped here. Without a stacking context an
|
||||
// unhovered gallery tile (which has no `transform`) lets its feature icons'
|
||||
// z-index leak into the card's stacking context and paint over the `elements`
|
||||
// overlay.
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
|
||||
transition: transform 0.2s linear;
|
||||
|
||||
@@ -18,3 +18,11 @@ $z-index-menu: 4;
|
||||
$z-index-notification: 4;
|
||||
$z-index-drawer: 3;
|
||||
$z-index-status-bar: 2;
|
||||
|
||||
// The call controls overlay sits above the live media (a peer of the status
|
||||
// bar) but below the menu, drawer and other card chrome.
|
||||
$z-index-call-controls: 2;
|
||||
|
||||
// Picture elements overlay every view's content (which paints at z-index
|
||||
// `auto`), so they always win against media/gallery/thumbnail surfaces.
|
||||
$z-index-elements: 1;
|
||||
|
||||
Reference in New Issue
Block a user