mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-22 07:27:36 +00:00
Compare commits
2 Commits
dev
...
feature-la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309d703cfd | ||
|
|
c0fd022d7a |
@@ -1,19 +0,0 @@
|
||||
export const startMediaProgressInterval = (
|
||||
interval: number | undefined,
|
||||
callback: () => void,
|
||||
intervalMs = 1000
|
||||
): number => {
|
||||
if (interval) {
|
||||
return interval;
|
||||
}
|
||||
return window.setInterval(callback, intervalMs);
|
||||
};
|
||||
|
||||
export const stopMediaProgressInterval = (
|
||||
interval: number | undefined
|
||||
): number | undefined => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -1,186 +0,0 @@
|
||||
import type { HaSlider } from "../../components/ha-slider";
|
||||
|
||||
interface VolumeSliderControllerOptions {
|
||||
getSlider: () => HaSlider | undefined;
|
||||
step: number;
|
||||
onSetVolume: (value: number) => void;
|
||||
onSetVolumeDebounced?: (value: number) => void;
|
||||
onValueUpdated?: (value: number) => void;
|
||||
}
|
||||
|
||||
export class VolumeSliderController {
|
||||
private _touchStartX = 0;
|
||||
|
||||
private _touchStartY = 0;
|
||||
|
||||
private _touchStartValue = 0;
|
||||
|
||||
private _touchDragging = false;
|
||||
|
||||
private _touchScrolling = false;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _lastValue = 0;
|
||||
|
||||
private _options: VolumeSliderControllerOptions;
|
||||
|
||||
constructor(options: VolumeSliderControllerOptions) {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public get isInteracting(): boolean {
|
||||
return this._touchDragging || this._dragging;
|
||||
}
|
||||
|
||||
public setStep(step: number): void {
|
||||
this._options.step = step;
|
||||
}
|
||||
|
||||
public handleInput = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = true;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolumeDebounced?.(value);
|
||||
};
|
||||
|
||||
public handleChange = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = false;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolume(value);
|
||||
};
|
||||
|
||||
public handleTouchStart = (ev: TouchEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const touch = ev.touches[0];
|
||||
this._touchStartX = touch.clientX;
|
||||
this._touchStartY = touch.clientY;
|
||||
this._touchStartValue = this._getSliderValue();
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._showTooltip();
|
||||
};
|
||||
|
||||
public handleTouchMove = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
return;
|
||||
}
|
||||
const touch = ev.touches[0];
|
||||
const deltaX = touch.clientX - this._touchStartX;
|
||||
const deltaY = touch.clientY - this._touchStartY;
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
if (!this._touchDragging) {
|
||||
if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) {
|
||||
this._touchScrolling = true;
|
||||
return;
|
||||
}
|
||||
if (absDeltaX > 8) {
|
||||
this._touchDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._touchDragging) {
|
||||
ev.preventDefault();
|
||||
const newValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
public handleTouchEnd = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
this._touchScrolling = false;
|
||||
this._hideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = ev.changedTouches[0];
|
||||
if (!this._touchDragging) {
|
||||
const tapValue = this._getVolumeFromTouch(touch.clientX);
|
||||
const delta =
|
||||
tapValue > this._touchStartValue
|
||||
? this._options.step
|
||||
: -this._options.step;
|
||||
const newValue = this._roundVolumeValue(this._touchStartValue + delta);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
} else {
|
||||
const finalValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(finalValue);
|
||||
this._options.onSetVolume(finalValue);
|
||||
}
|
||||
|
||||
this._touchDragging = false;
|
||||
this._dragging = false;
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleTouchCancel = (): void => {
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._dragging = false;
|
||||
this._updateValue(this._touchStartValue);
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleWheel = (ev: WheelEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const direction = ev.deltaY > 0 ? -1 : 1;
|
||||
const currentValue = this._getSliderValue();
|
||||
const newValue = this._roundVolumeValue(
|
||||
currentValue + direction * this._options.step
|
||||
);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
};
|
||||
|
||||
private _getVolumeFromTouch(clientX: number): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (!slider) {
|
||||
return 0;
|
||||
}
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
|
||||
const percentage = (x / rect.width) * 100;
|
||||
return this._roundVolumeValue(percentage);
|
||||
}
|
||||
|
||||
private _roundVolumeValue(value: number): number {
|
||||
return Math.min(
|
||||
Math.max(Math.round(value / this._options.step) * this._options.step, 0),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
private _getSliderValue(): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
return Number(slider.value);
|
||||
}
|
||||
return this._lastValue;
|
||||
}
|
||||
|
||||
private _updateValue(value: number): void {
|
||||
this._lastValue = value;
|
||||
this._options.onValueUpdated?.(value);
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
slider.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private _showTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.showTooltip?.();
|
||||
}
|
||||
|
||||
private _hideTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.hideTooltip?.();
|
||||
}
|
||||
}
|
||||
@@ -423,17 +423,12 @@ export const formatMediaTime = (seconds: number | undefined): string => {
|
||||
return "";
|
||||
}
|
||||
|
||||
const totalSeconds = Math.max(0, Math.floor(seconds));
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const secs = totalSeconds % 60;
|
||||
const pad = (value: number) => value.toString().padStart(2, "0");
|
||||
|
||||
if (hours > 0) {
|
||||
return `${pad(hours)}:${pad(minutes)}:${pad(secs)}`;
|
||||
}
|
||||
|
||||
return `${pad(minutes)}:${pad(secs)}`;
|
||||
let secondsString = new Date(seconds * 1000).toISOString();
|
||||
secondsString =
|
||||
seconds > 3600
|
||||
? secondsString.substring(11, 16)
|
||||
: secondsString.substring(14, 19);
|
||||
return secondsString.replace(/^0+/, "").padStart(4, "0");
|
||||
};
|
||||
|
||||
export const cleanupMediaTitle = (title?: string): string | undefined => {
|
||||
|
||||
@@ -14,14 +14,9 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { formatDurationDigital } from "../../../common/datetime/format_duration";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import {
|
||||
startMediaProgressInterval,
|
||||
stopMediaProgressInterval,
|
||||
} from "../../../common/util/media-progress";
|
||||
import { VolumeSliderController } from "../../../common/util/volume-slider";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -43,7 +38,6 @@ import {
|
||||
cleanupMediaTitle,
|
||||
computeMediaControls,
|
||||
computeMediaDescription,
|
||||
formatMediaTime,
|
||||
handleMediaControlClick,
|
||||
MediaPlayerEntityFeature,
|
||||
mediaPlayerPlayMedia,
|
||||
@@ -60,34 +54,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
@query("#position-slider")
|
||||
private _positionSlider?: HaSlider;
|
||||
|
||||
@query(".volume-slider")
|
||||
private _volumeSlider?: HaSlider;
|
||||
|
||||
private _progressInterval?: number;
|
||||
|
||||
private _volumeStep = 2;
|
||||
|
||||
private _debouncedVolumeSet = debounce((value: number) => {
|
||||
this._setVolume(value);
|
||||
}, 100);
|
||||
|
||||
private _volumeController = new VolumeSliderController({
|
||||
getSlider: () => this._volumeSlider,
|
||||
step: this._volumeStep,
|
||||
onSetVolume: (value) => this._setVolume(value),
|
||||
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
|
||||
});
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._syncProgressInterval();
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearProgressInterval();
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues) {
|
||||
if (this._positionSlider) {
|
||||
this._positionSlider.valueFormatter = (value: number) =>
|
||||
@@ -96,7 +62,14 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _formatDuration(duration: number) {
|
||||
return formatMediaTime(duration);
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = Math.floor(duration % 60);
|
||||
return formatDurationDigital(this.hass.locale, {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
})!;
|
||||
}
|
||||
|
||||
protected _renderVolumeControl() {
|
||||
@@ -166,25 +139,13 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
${!supportsMute
|
||||
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<div
|
||||
class="volume-slider-container"
|
||||
@touchstart=${this._volumeController.handleTouchStart}
|
||||
@touchmove=${this._volumeController.handleTouchMove}
|
||||
@touchend=${this._volumeController.handleTouchEnd}
|
||||
@touchcancel=${this._volumeController.handleTouchCancel}
|
||||
@wheel=${this._volumeController.handleWheel}
|
||||
>
|
||||
<ha-slider
|
||||
class="volume-slider"
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
.step=${this._volumeStep}
|
||||
@input=${this._volumeController.handleInput}
|
||||
@change=${this._volumeController.handleChange}
|
||||
></ha-slider>
|
||||
</div>
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -300,17 +261,17 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj, true);
|
||||
const coverUrlRaw =
|
||||
const coverUrl =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture ||
|
||||
"";
|
||||
const coverUrl = coverUrlRaw ? this.hass.hassUrl(coverUrlRaw) : "";
|
||||
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
|
||||
|
||||
const position = Math.max(Math.floor(playerObj.currentProgress || 0), 0);
|
||||
const duration = Math.max(stateObj.attributes.media_duration || 0, 0);
|
||||
const remaining = Math.max(duration - position, 0);
|
||||
const remainingFormatted = this._formatDuration(remaining);
|
||||
const positionFormatted = this._formatDuration(position);
|
||||
const durationFormatted = this._formatDuration(duration);
|
||||
const primaryTitle = cleanupMediaTitle(stateObj.attributes.media_title);
|
||||
const secondaryTitle = computeMediaDescription(stateObj);
|
||||
const turnOn = controls?.find((c) => c.action === "turn_on");
|
||||
@@ -370,12 +331,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
?disabled=${!stateActive(stateObj) ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
|
||||
>
|
||||
<span class="position-time" slot="reference"
|
||||
>${positionFormatted}</span
|
||||
>
|
||||
<span class="position-time" slot="reference"
|
||||
>${durationFormatted}</span
|
||||
>
|
||||
<span slot="reference">${positionFormatted}</span>
|
||||
<span slot="reference">${remainingFormatted}</span>
|
||||
</ha-slider>
|
||||
</div>
|
||||
`
|
||||
@@ -574,16 +531,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
margin-left: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.volume-slider {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.volume ha-svg-icon {
|
||||
padding: var(--ha-space-1);
|
||||
height: 16px;
|
||||
@@ -621,10 +568,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.position-time {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.media-info-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -679,39 +622,6 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("stateObj")) {
|
||||
this._syncProgressInterval();
|
||||
}
|
||||
}
|
||||
|
||||
private _syncProgressInterval(): void {
|
||||
if (this._shouldUpdateProgress()) {
|
||||
this._progressInterval = startMediaProgressInterval(
|
||||
this._progressInterval,
|
||||
() => this.requestUpdate()
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._clearProgressInterval();
|
||||
}
|
||||
|
||||
private _clearProgressInterval(): void {
|
||||
this._progressInterval = stopMediaProgressInterval(this._progressInterval);
|
||||
}
|
||||
|
||||
private _shouldUpdateProgress(): boolean {
|
||||
const stateObj = this.stateObj;
|
||||
return (
|
||||
!!stateObj &&
|
||||
stateObj.state === "playing" &&
|
||||
Number(stateObj.attributes.media_duration) > 0 &&
|
||||
"media_position" in stateObj.attributes &&
|
||||
"media_position_updated_at" in stateObj.attributes
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleMute() {
|
||||
this.hass!.callService("media_player", "volume_mute", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
@@ -719,10 +629,10 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _setVolume(value: number) {
|
||||
private _selectedValueChanged(e: Event): void {
|
||||
this.hass!.callService("media_player", "volume_set", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
volume_level: value / 100,
|
||||
volume_level: (e.target as any).value / 100,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,25 +5,29 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-analytics";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-settings-row";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import type { Analytics } from "../../../data/analytics";
|
||||
import {
|
||||
getAnalyticsDetails,
|
||||
setAnalyticsPreferences,
|
||||
} from "../../../data/analytics";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
import { subscribeLabFeature } from "../../../data/labs";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-alert";
|
||||
|
||||
@customElement("ha-config-analytics")
|
||||
class ConfigAnalytics extends LitElement {
|
||||
class ConfigAnalytics extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _analyticsDetails?: Analytics;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _snapshotsLabEnabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const error = this._error
|
||||
? this._error
|
||||
@@ -56,8 +60,7 @@ class ConfigAnalytics extends LitElement {
|
||||
></ha-analytics>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this._analyticsDetails &&
|
||||
"snapshots" in this._analyticsDetails.preferences
|
||||
${this._snapshotsLabEnabled
|
||||
? html`<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
@@ -70,22 +73,16 @@ class ConfigAnalytics extends LitElement {
|
||||
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/device-database/")}
|
||||
href="https://www.openhomefoundation.org/device-database-data-use-statement"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.learn_more"
|
||||
"ui.panel.config.analytics.preferences.snapshots.data_use_statement"
|
||||
)}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-alert
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.alert.title"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.alert.content"
|
||||
)}</ha-alert
|
||||
>
|
||||
"ui.panel.config.analytics.preferences.snapshots.data_use_statement_suffix"
|
||||
)}
|
||||
</p>
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
@@ -111,6 +108,19 @@ class ConfigAnalytics extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass.connection,
|
||||
"analytics",
|
||||
"snapshots",
|
||||
(feature: LabPreviewFeature) => {
|
||||
this._snapshotsLabEnabled = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (isComponentLoaded(this.hass, "analytics")) {
|
||||
|
||||
@@ -105,19 +105,10 @@ class AddIntegrationDialog extends LitElement {
|
||||
const loadPromise = this._load();
|
||||
|
||||
if (params?.domain) {
|
||||
// If we get here we clicked the button to add an entry for a specific integration
|
||||
// If there is discovery in process, show this dialog to select a new flow
|
||||
// or continue an existing flow.
|
||||
// If no flow in process, just open the config flow dialog directly
|
||||
// Just open the config flow dialog, do not show this dialog
|
||||
await loadPromise;
|
||||
const flowsInProgress = this._getFlowsInProgressForDomains([
|
||||
params.domain,
|
||||
]);
|
||||
|
||||
if (!flowsInProgress.length) {
|
||||
await this._createFlow(params.domain);
|
||||
return;
|
||||
}
|
||||
await this._createFlow(params.domain);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params?.brand === "_discovered") {
|
||||
@@ -126,12 +117,10 @@ class AddIntegrationDialog extends LitElement {
|
||||
this._showDiscovered = true;
|
||||
}
|
||||
|
||||
// Only open the dialog if no domain is provided or we need to select a flow
|
||||
// Only open the dialog if no domain is provided
|
||||
this._open = true;
|
||||
this._pickedBrand =
|
||||
params?.brand === "_discovered"
|
||||
? undefined
|
||||
: params?.domain || params?.brand;
|
||||
params?.brand === "_discovered" ? undefined : params?.brand;
|
||||
this._initialFilter = params?.initialFilter;
|
||||
this._navigateToResult = params?.navigateToResult ?? false;
|
||||
this._narrow = matchMedia(
|
||||
|
||||
@@ -5,31 +5,31 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
labsUpdatePreviewFeature,
|
||||
subscribeLabFeatures,
|
||||
} from "../../../data/labs";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
|
||||
import {
|
||||
showLabsProgressDialog,
|
||||
closeLabsProgressDialog,
|
||||
} from "./show-dialog-labs-progress";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-switch";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LabPreviewFeature } from "../../../data/labs";
|
||||
import {
|
||||
labsUpdatePreviewFeature,
|
||||
subscribeLabFeatures,
|
||||
} from "../../../data/labs";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
|
||||
import {
|
||||
closeLabsProgressDialog,
|
||||
showLabsProgressDialog,
|
||||
} from "./show-dialog-labs-progress";
|
||||
|
||||
@customElement("ha-config-labs")
|
||||
class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
@@ -42,21 +42,31 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
@state() private _highlightedPreviewFeature?: string;
|
||||
|
||||
private _sortedPreviewFeatures = memoizeOne(
|
||||
(localize: LocalizeFunc, features: LabPreviewFeature[]) =>
|
||||
// Sort by localized integration name alphabetically
|
||||
[...features].sort((a, b) =>
|
||||
domainToName(localize, a.domain).localeCompare(
|
||||
(localize: LocalizeFunc, features: LabPreviewFeature[]) => {
|
||||
const featuresToSort = [...features];
|
||||
|
||||
return featuresToSort.sort((a, b) => {
|
||||
// Place frontend.winter_mode at the bottom
|
||||
if (a.domain === "frontend" && a.preview_feature === "winter_mode")
|
||||
return 1;
|
||||
if (b.domain === "frontend" && b.preview_feature === "winter_mode")
|
||||
return -1;
|
||||
|
||||
// Sort everything else alphabetically
|
||||
return domainToName(localize, a.domain).localeCompare(
|
||||
domainToName(localize, b.domain)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeatures(this.hass.connection, (features) => {
|
||||
// Load title translations for integrations with preview features
|
||||
// Load title and preview_features translations for integrations with preview features
|
||||
const domains = [...new Set(features.map((f) => f.domain))];
|
||||
this.hass.loadBackendTranslation("title", domains);
|
||||
this.hass.loadBackendTranslation("preview_features", domains);
|
||||
|
||||
this._preview_features = features;
|
||||
}),
|
||||
@@ -165,6 +175,8 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
private _renderPreviewFeature(
|
||||
preview_feature: LabPreviewFeature
|
||||
): TemplateResult {
|
||||
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
|
||||
|
||||
const featureName = this.hass.localize(
|
||||
`component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.name`
|
||||
);
|
||||
@@ -182,7 +194,6 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
? `${integrationName} • ${this.hass.localize("ui.panel.config.labs.custom_integration")}`
|
||||
: integrationName;
|
||||
|
||||
const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`;
|
||||
const isHighlighted = this._highlightedPreviewFeature === previewFeatureId;
|
||||
|
||||
// Build description with learn more link if available
|
||||
|
||||
@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import { updateDeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import {
|
||||
fetchFrontendSystemData,
|
||||
saveFrontendSystemData,
|
||||
@@ -15,7 +14,6 @@ import { showToast } from "../../util/toast";
|
||||
import "../lovelace/hui-root";
|
||||
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
@@ -97,29 +95,6 @@ class PanelHome extends LitElement {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _handleLLCustomEvent = (ev: Event) => {
|
||||
const detail = (ev as CustomEvent).detail;
|
||||
if (detail.home_panel) {
|
||||
const { type, device_id } = detail.home_panel;
|
||||
if (type === "assign_area") {
|
||||
this._showAssignAreaDialog(device_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _showAssignAreaDialog(deviceId: string) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, deviceId, updates);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._lovelace) {
|
||||
return nothing;
|
||||
@@ -132,7 +107,6 @@ class PanelHome extends LitElement {
|
||||
.lovelace=${this._lovelace}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
@ll-custom=${this._handleLLCustomEvent}
|
||||
></hui-root>
|
||||
`;
|
||||
}
|
||||
@@ -142,7 +116,6 @@ class PanelHome extends LitElement {
|
||||
strategy: {
|
||||
type: "home",
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
home_panel: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.content:hover ha-icon-next {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "../heading-badges/hui-button-heading-badge";
|
||||
import "../heading-badges/hui-entity-heading-badge";
|
||||
|
||||
import {
|
||||
@@ -7,7 +6,7 @@ import {
|
||||
} from "./create-element-base";
|
||||
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
|
||||
const ALWAYS_LOADED_TYPES = new Set(["error", "entity", "button"]);
|
||||
const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
|
||||
|
||||
export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
|
||||
createLovelaceElement(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -5,12 +6,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-grid-size-picker";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-slider";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@@ -92,10 +94,14 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
"ui.panel.lovelace.editor.edit_card.layout.explanation"
|
||||
)}
|
||||
</p>
|
||||
<ha-dropdown
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
placement="bottom-end"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
.corner=${"BOTTOM_END"}
|
||||
menu-corner="END"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -104,13 +110,13 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml" .disabled=${!this._uiAvailable}>
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
${this._yamlMode
|
||||
? html`
|
||||
@@ -238,11 +244,11 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "toggle_yaml") {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +331,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.header ha-dropdown {
|
||||
.header ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
@@ -16,15 +16,15 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
@@ -126,11 +126,14 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition}
|
||||
</h3>
|
||||
<ha-dropdown
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@click=${stopPropagation}
|
||||
placement="bottom-end"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
.corner=${"BOTTOM_END"}
|
||||
menu-corner="END"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -139,50 +142,54 @@ export class HaCardConditionEditor extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="test">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.test"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="duplicate">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="copy">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item value="cut">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml"
|
||||
.disabled=${!this._uiAvailable}
|
||||
>
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-dropdown-item variant="danger" value="delete">
|
||||
<ha-list-item class="warning" graphic="icon">
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
${!this._uiAvailable
|
||||
? html`
|
||||
<ha-alert
|
||||
@@ -245,31 +252,26 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "test":
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
await this._testCondition();
|
||||
return;
|
||||
case "duplicate":
|
||||
break;
|
||||
case 1:
|
||||
this._duplicateCondition();
|
||||
return;
|
||||
case "copy":
|
||||
break;
|
||||
case 2:
|
||||
this._copyCondition();
|
||||
return;
|
||||
case "cut":
|
||||
break;
|
||||
case 3:
|
||||
this._cutCondition();
|
||||
return;
|
||||
case "toggle_yaml":
|
||||
break;
|
||||
case 4:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
return;
|
||||
case "delete":
|
||||
break;
|
||||
case 5:
|
||||
this._delete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,9 +321,9 @@ export class HaCardConditionEditor extends LitElement {
|
||||
this._delete();
|
||||
}
|
||||
|
||||
private _delete = () => {
|
||||
private _delete() {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
};
|
||||
}
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -335,7 +337,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
ha-dropdown {
|
||||
ha-button-menu {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-expansion-panel {
|
||||
|
||||
@@ -3,12 +3,11 @@ import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
@@ -28,6 +27,7 @@ import "./types/ha-card-condition-screen";
|
||||
import "./types/ha-card-condition-state";
|
||||
import "./types/ha-card-condition-time";
|
||||
import "./types/ha-card-condition-user";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
const UI_CONDITION = [
|
||||
"location",
|
||||
@@ -41,6 +41,8 @@ const UI_CONDITION = [
|
||||
"or",
|
||||
] as const satisfies readonly Condition["condition"][];
|
||||
|
||||
export const PASTE_VALUE = "__paste__" as const;
|
||||
|
||||
@customElement("ha-card-conditions-editor")
|
||||
export class HaCardConditionsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -105,7 +107,11 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-dropdown @wa-select=${this._addCondition}>
|
||||
<ha-button-menu
|
||||
@action=${this._addCondition}
|
||||
fixed
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger" appearance="filled">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
@@ -114,57 +120,59 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
</ha-button>
|
||||
${this._clipboard
|
||||
? html`
|
||||
<ha-dropdown-item value="paste">
|
||||
<ha-list-item .value=${PASTE_VALUE} graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste_condition"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="graphic"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${UI_CONDITION.map(
|
||||
(condition) => html`
|
||||
<ha-dropdown-item .value=${condition}>
|
||||
<ha-list-item .value=${condition} graphic="icon">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition}.label`
|
||||
) || condition}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="graphic"
|
||||
.path=${ICON_CONDITION[condition]}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addCondition(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const condition = ev.detail.item.value as "paste" | Condition["condition"];
|
||||
private _addCondition(ev: CustomEvent): void {
|
||||
const conditions = [...this.conditions];
|
||||
|
||||
if (!condition || (condition === "paste" && !this._clipboard)) {
|
||||
const item = (ev.currentTarget as HaSelect).items[ev.detail.index];
|
||||
|
||||
if (item.value === PASTE_VALUE && this._clipboard) {
|
||||
const condition = deepClone(this._clipboard);
|
||||
conditions.push(condition);
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
return;
|
||||
}
|
||||
|
||||
if (condition === "paste") {
|
||||
const newCondition = deepClone(this._clipboard);
|
||||
conditions.push(newCondition);
|
||||
} else {
|
||||
const elClass = customElements.get(`ha-card-condition-${condition}`) as
|
||||
| LovelaceConditionEditorConstructor
|
||||
| undefined;
|
||||
const condition = item.value as Condition["condition"];
|
||||
|
||||
conditions.push(
|
||||
elClass?.defaultConfig ? { ...elClass.defaultConfig } : { condition }
|
||||
);
|
||||
}
|
||||
const elClass = customElements.get(`ha-card-condition-${condition}`) as
|
||||
| LovelaceConditionEditorConstructor
|
||||
| undefined;
|
||||
|
||||
conditions.push(
|
||||
elClass?.defaultConfig
|
||||
? { ...elClass.defaultConfig }
|
||||
: { condition: condition }
|
||||
);
|
||||
this._focusLastConditionOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
@@ -202,9 +210,8 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
margin-top: 12px;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
margin-top: var(--ha-space-3);
|
||||
ha-button-menu {
|
||||
margin-top: 12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDragHorizontalVariant,
|
||||
@@ -9,11 +8,10 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { CustomCardFeatureEntry } from "../../../../data/lovelace_custom_cards";
|
||||
@@ -26,7 +24,6 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
||||
import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-card-feature";
|
||||
import { supportsBarGaugeCardFeature } from "../../card-features/hui-bar-gauge-card-feature";
|
||||
import { supportsButtonCardFeature } from "../../card-features/hui-button-card-feature";
|
||||
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
||||
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
|
||||
@@ -55,14 +52,15 @@ import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features
|
||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||
import { supportsTrendGraphCardFeature } from "../../card-features/hui-trend-graph-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||
import { supportsTrendGraphCardFeature } from "../../card-features/hui-trend-graph-card-feature";
|
||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
|
||||
import { supportsValvePositionCardFeature } from "../../card-features/hui-valve-position-card-feature";
|
||||
import { supportsBarGaugeCardFeature } from "../../card-features/hui-bar-gauge-card-feature";
|
||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
@@ -406,39 +404,45 @@ export class HuiCardFeaturesEditor extends LitElement {
|
||||
</ha-sortable>
|
||||
${supportedFeaturesType.length > 0
|
||||
? html`
|
||||
<ha-dropdown @wa-select=${this._addFeature}>
|
||||
<ha-button-menu
|
||||
fixed
|
||||
@action=${this._addFeature}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger" appearance="filled" size="small">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass!.localize(`ui.panel.lovelace.editor.features.add`)}
|
||||
</ha-button>
|
||||
${types.map(
|
||||
(type) => html`
|
||||
<ha-dropdown-item .value=${type}>
|
||||
<ha-list-item .value=${type}>
|
||||
${this._getFeatureTypeLabel(type)}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
${types.length > 0 && customTypes.length > 0
|
||||
? html`<wa-divider></wa-divider>`
|
||||
? html`<li divider role="separator"></li>`
|
||||
: nothing}
|
||||
${customTypes.map(
|
||||
(type) => html`
|
||||
<ha-dropdown-item .value=${type}>
|
||||
<ha-list-item .value=${type}>
|
||||
${this._getFeatureTypeLabel(type)}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _addFeature(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const value = ev.detail.item.value as FeatureType;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
private async _addFeature(ev: CustomEvent): Promise<void> {
|
||||
const index = ev.detail.index as number;
|
||||
|
||||
if (index == null) return;
|
||||
|
||||
const value = this._getSupportedFeaturesType()[index];
|
||||
if (!value) return;
|
||||
|
||||
const elClass = await getCardFeatureElementClass(value);
|
||||
|
||||
@@ -495,9 +499,7 @@ export class HuiCardFeaturesEditor extends LitElement {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
ha-button-menu {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.feature {
|
||||
|
||||
@@ -1,33 +1,21 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDragHorizontalVariant,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import type { HaEntityPicker } from "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element";
|
||||
import type {
|
||||
ButtonHeadingBadgeConfig,
|
||||
EntityHeadingBadgeConfig,
|
||||
LovelaceHeadingBadgeConfig,
|
||||
} from "../../heading-badges/types";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
|
||||
const UI_BADGE_TYPES = ["entity", "button"] as const;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -53,12 +41,8 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
return this._badgesKeys.get(badge)!;
|
||||
}
|
||||
|
||||
private _getBadgeTypeLabel(type: string): string {
|
||||
return (
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.heading-badges.types.${type}.label`
|
||||
) || type
|
||||
);
|
||||
private _createValueChangedHandler(index: number) {
|
||||
return (ev: CustomEvent) => this._valueChanged(ev, index);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -67,186 +51,120 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.badges?.length
|
||||
${this.badges
|
||||
? html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._badgeMoved}
|
||||
>
|
||||
<div class="badges">
|
||||
<div class="entities">
|
||||
${repeat(
|
||||
this.badges.filter(Boolean),
|
||||
this.badges,
|
||||
(badge) => this._getKey(badge),
|
||||
(badge, index) => this._renderBadgeItem(badge, index)
|
||||
(badge, index) => {
|
||||
const type = badge.type ?? "entity";
|
||||
const isEntityBadge =
|
||||
type === "entity" && "entity" in badge;
|
||||
const entityBadge = isEntityBadge
|
||||
? (badge as EntityHeadingBadgeConfig)
|
||||
: undefined;
|
||||
return html`
|
||||
<div class="badge">
|
||||
<div class="handle">
|
||||
<ha-svg-icon
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
${isEntityBadge && entityBadge
|
||||
? html`
|
||||
<ha-entity-picker
|
||||
hide-clear-icon
|
||||
.hass=${this.hass}
|
||||
.value=${entityBadge.entity ?? ""}
|
||||
@value-changed=${this._createValueChangedHandler(
|
||||
index
|
||||
)}
|
||||
></ha-entity-picker>
|
||||
`
|
||||
: html`
|
||||
<div class="badge-content">
|
||||
<span>${type}</span>
|
||||
</div>
|
||||
`}
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.edit`
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editBadge}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.remove`
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="remove-icon"
|
||||
.index=${index}
|
||||
@click=${this._removeEntity}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`
|
||||
: nothing}
|
||||
<ha-button-menu
|
||||
fixed
|
||||
@action=${this._addBadge}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-button slot="trigger" appearance="filled" size="small">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(`ui.panel.lovelace.editor.heading-badges.add`)}
|
||||
</ha-button>
|
||||
${UI_BADGE_TYPES.map(
|
||||
(type) => html`
|
||||
<ha-list-item .value=${type}>
|
||||
${this._getBadgeTypeLabel(type)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderBadgeItem(badge: LovelaceHeadingBadgeConfig, index: number) {
|
||||
const type = badge.type ?? "entity";
|
||||
const entityBadge = badge as EntityHeadingBadgeConfig;
|
||||
const isWarning =
|
||||
type === "entity" &&
|
||||
(!entityBadge.entity || !this.hass.states[entityBadge.entity]);
|
||||
|
||||
return html`
|
||||
<div class=${classMap({ badge: true, warning: isWarning })}>
|
||||
<div class="handle">
|
||||
<ha-svg-icon .path=${mdiDragHorizontalVariant}></ha-svg-icon>
|
||||
</div>
|
||||
${type === "entity"
|
||||
? this._renderEntityBadge(entityBadge)
|
||||
: type === "button"
|
||||
? this._renderButtonBadge(badge as ButtonHeadingBadgeConfig)
|
||||
: this._renderUnknownBadge(type)}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(`ui.panel.lovelace.editor.badges.edit`)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editBadge}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(`ui.panel.lovelace.editor.badges.remove`)}
|
||||
.path=${mdiDelete}
|
||||
class="remove-icon"
|
||||
.index=${index}
|
||||
@click=${this._removeBadge}
|
||||
></ha-icon-button>
|
||||
<div class="add-container">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.choose_entity"
|
||||
)}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.choose_entity"
|
||||
)}
|
||||
@value-changed=${this._entityPicked}
|
||||
.value=${undefined}
|
||||
@click=${preventDefault}
|
||||
add-button
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEntityBadge(badge: EntityHeadingBadgeConfig) {
|
||||
const entityId = badge.entity;
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
if (!entityId) {
|
||||
return html`
|
||||
<div class="badge-content">
|
||||
<div>
|
||||
<span>${this._getBadgeTypeLabel("entity")}</span>
|
||||
<span class="secondary"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.heading-badges.no_entity"
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
private _entityPicked(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<div class="badge-content">
|
||||
<div>
|
||||
<span>${entityId}</span>
|
||||
<span class="secondary"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.heading-badges.entity_not_found"
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [entityName ? deviceName : undefined, areaName]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
|
||||
return html`
|
||||
<div class="badge-content">
|
||||
<div>
|
||||
<span>${primary}</span>
|
||||
${secondary
|
||||
? html`<span class="secondary">${secondary}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const newEntity: LovelaceHeadingBadgeConfig = {
|
||||
type: "entity",
|
||||
entity: ev.detail.value,
|
||||
};
|
||||
const newBadges = [...(this.badges || []), newEntity];
|
||||
(ev.target as HaEntityPicker).value = undefined;
|
||||
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||
}
|
||||
|
||||
private _renderButtonBadge(badge: ButtonHeadingBadgeConfig) {
|
||||
return html`
|
||||
<div class="badge-content">
|
||||
<div>
|
||||
<span>${this._getBadgeTypeLabel("button")}</span>
|
||||
${badge.text
|
||||
? html`<span class="secondary">${badge.text}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
private _valueChanged(ev: CustomEvent, index: number): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const newBadges = [...(this.badges || [])];
|
||||
|
||||
private _renderUnknownBadge(type: string) {
|
||||
return html`
|
||||
<div class="badge-content">
|
||||
<div>
|
||||
<span>${type}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _addBadge(ev: CustomEvent): Promise<void> {
|
||||
const index = ev.detail.index as number;
|
||||
|
||||
if (index == null) return;
|
||||
|
||||
const type = UI_BADGE_TYPES[index];
|
||||
if (!type) return;
|
||||
|
||||
const elClass = await getHeadingBadgeElementClass(type);
|
||||
|
||||
let newBadge: LovelaceHeadingBadgeConfig;
|
||||
if (elClass && elClass.getStubConfig) {
|
||||
newBadge = elClass.getStubConfig(this.hass);
|
||||
if (!value) {
|
||||
newBadges.splice(index, 1);
|
||||
} else {
|
||||
newBadge = { type } as LovelaceHeadingBadgeConfig;
|
||||
newBadges[index] = {
|
||||
...newBadges[index],
|
||||
entity: value,
|
||||
};
|
||||
}
|
||||
|
||||
const newBadges = [...(this.badges || []), newBadge];
|
||||
|
||||
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||
|
||||
await nextRender();
|
||||
// Open the editor for the new badge
|
||||
fireEvent(this, "edit-heading-badge", { index: newBadges.length - 1 });
|
||||
}
|
||||
|
||||
private _badgeMoved(ev: CustomEvent): void {
|
||||
@@ -259,7 +177,7 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||
}
|
||||
|
||||
private _removeBadge(ev: CustomEvent): void {
|
||||
private _removeEntity(ev: CustomEvent): void {
|
||||
const index = (ev.currentTarget as any).index;
|
||||
const newBadges = [...(this.badges || [])];
|
||||
|
||||
@@ -280,12 +198,11 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-button-menu {
|
||||
ha-button {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.badges {
|
||||
.entities {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
@@ -295,7 +212,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge .handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
@@ -304,14 +220,13 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.badge .handle > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.badge-content {
|
||||
height: var(--ha-space-12);
|
||||
font-size: var(--ha-font-size-m);
|
||||
height: 60px;
|
||||
font-size: var(--ha-font-size-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@@ -323,9 +238,15 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.badge ha-entity-picker {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.remove-icon,
|
||||
.edit-icon {
|
||||
--mdc-icon-button-size: var(--ha-space-9);
|
||||
--mdc-icon-button-size: 36px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -334,19 +255,24 @@ export class HuiHeadingBadgesEditor extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.badge.warning {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.badge.warning .secondary {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
|
||||
li[divider] {
|
||||
border-bottom-color: var(--divider-color);
|
||||
}
|
||||
|
||||
.add-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
mwc-menu-surface {
|
||||
--mdc-menu-min-width: 100%;
|
||||
}
|
||||
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,9 @@ export class HuiHeadingCardEditor
|
||||
<ha-expansion-panel outlined>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiListBox}></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.card.heading.badges")}
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.heading.entities"
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
<hui-heading-badges-editor
|
||||
|
||||
@@ -9,13 +9,13 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-button-menu";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import "../../../../../components/ha-dialog-header";
|
||||
import "../../../../../components/ha-dropdown";
|
||||
import "../../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../../components/ha-dropdown-item";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import type { LovelaceStrategyConfig } from "../../../../../data/lovelace/config/strategy";
|
||||
import {
|
||||
haStyleDialog,
|
||||
@@ -98,18 +98,13 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "toggle-mode":
|
||||
private _handleAction(ev) {
|
||||
ev.stopPropagation();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._toggleMode();
|
||||
break;
|
||||
case "take-control":
|
||||
case 1:
|
||||
this._takeControl();
|
||||
break;
|
||||
}
|
||||
@@ -155,32 +150,41 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
${this._params.title
|
||||
? html`<span slot="subtitle">${this._params.title}</span>`
|
||||
: nothing}
|
||||
<ha-dropdown
|
||||
placement="bottom-end"
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
slot="actionItems"
|
||||
@wa-select=${this._handleAction}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item
|
||||
value="toggle-mode"
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.disabled=${!this._guiModeAvailable && !this._GUImode}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._GUImode ? "ui" : "yaml"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="take-control">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy-editor.take_control"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiAccountHardHat}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiAccountHardHat}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dialog-header>
|
||||
<div class="content">
|
||||
<hui-dashboard-strategy-element-editor
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
import { mdiEye, mdiGestureTap } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { any, array, assert, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import type { ButtonHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
import type { LovelaceGenericElementEditor } from "../../types";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
|
||||
const buttonConfigStruct = object({
|
||||
type: optional(string()),
|
||||
text: optional(string()),
|
||||
icon: optional(string()),
|
||||
color: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
visibility: optional(array(any())),
|
||||
});
|
||||
|
||||
@customElement("hui-button-heading-badge-editor")
|
||||
export class HuiButtonHeadingBadgeEditor
|
||||
extends LitElement
|
||||
implements LovelaceGenericElementEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() private _config?: ButtonHeadingBadgeConfig;
|
||||
|
||||
public setConfig(config: ButtonHeadingBadgeConfig): void {
|
||||
assert(config, buttonConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "text",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: { icon: {} },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema();
|
||||
|
||||
const conditions = this._config.visibility ?? [];
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<ha-expansion-panel outlined>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiEye}></ha-svg-icon>
|
||||
<h3 slot="header">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.heading.button_config.visibility"
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.heading.button_config.visibility_explanation"
|
||||
)}
|
||||
</p>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@value-changed=${this._conditionChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = ev.detail.value as ButtonHeadingBadgeConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
|
||||
const newConfig: ButtonHeadingBadgeConfig = {
|
||||
...this._config,
|
||||
visibility: conditions,
|
||||
};
|
||||
if (newConfig.visibility?.length === 0) {
|
||||
delete newConfig.visibility;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config: newConfig });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "text":
|
||||
case "color":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.heading.button_config.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-button-heading-badge-editor": HuiButtonHeadingBadgeEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
@@ -10,15 +11,14 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
@@ -218,32 +218,38 @@ export class HuiDialogEditView extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<h2 slot="title">${this._viewConfigTitle}</h2>
|
||||
<ha-dropdown
|
||||
<ha-button-menu
|
||||
slot="actionItems"
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleAction}
|
||||
fixed
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
@action=${this._handleAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="toggle-mode">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="move-to-dashboard">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_dashboard"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
slot="graphic"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
${convertToSection
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
@@ -324,18 +330,14 @@ export class HuiDialogEditView extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "toggle-mode":
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case "move-to-dashboard":
|
||||
case 1:
|
||||
this._openSelectDashboard();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiClose, mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { deepEqual } from "../../../../common/util/deep-equal";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
@@ -113,23 +113,29 @@ export class HuiDialogEditViewHeader extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<h2 slot="title">${title}</h2>
|
||||
<ha-dropdown
|
||||
<ha-button-menu
|
||||
slot="actionItems"
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleAction}
|
||||
fixed
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
@action=${this._handleAction}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="toggle-mode">
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_header.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dialog-header>
|
||||
${content}
|
||||
<ha-button
|
||||
@@ -144,11 +150,13 @@ export class HuiDialogEditViewHeader extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "toggle-mode") {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-icon";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type {
|
||||
LovelaceHeadingBadge,
|
||||
LovelaceHeadingBadgeEditor,
|
||||
} from "../types";
|
||||
import type { ButtonHeadingBadgeConfig } from "./types";
|
||||
|
||||
const DEFAULT_ACTIONS: Pick<
|
||||
ButtonHeadingBadgeConfig,
|
||||
"tap_action" | "hold_action" | "double_tap_action"
|
||||
> = {
|
||||
tap_action: { action: "none" },
|
||||
hold_action: { action: "none" },
|
||||
double_tap_action: { action: "none" },
|
||||
};
|
||||
|
||||
@customElement("hui-button-heading-badge")
|
||||
export class HuiButtonHeadingBadge
|
||||
extends LitElement
|
||||
implements LovelaceHeadingBadge
|
||||
{
|
||||
public static async getConfigElement(): Promise<LovelaceHeadingBadgeEditor> {
|
||||
await import("../editor/heading-badge-editor/hui-button-heading-badge-editor");
|
||||
return document.createElement("hui-button-heading-badge-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): ButtonHeadingBadgeConfig {
|
||||
return {
|
||||
type: "button",
|
||||
icon: "mdi:gesture-tap-button",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: ButtonHeadingBadgeConfig;
|
||||
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
public setConfig(config: ButtonHeadingBadgeConfig): void {
|
||||
this._config = {
|
||||
...DEFAULT_ACTIONS,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
get hasAction() {
|
||||
return (
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const config = this._config;
|
||||
|
||||
const color = config.color ? computeCssColor(config.color) : undefined;
|
||||
|
||||
const style = { "--color": color };
|
||||
|
||||
return html`
|
||||
<ha-control-button
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
style=${styleMap(style)}
|
||||
.label=${config.text}
|
||||
class=${classMap({ colored: !!color, "with-text": !!config.text })}
|
||||
>
|
||||
<span class="content">
|
||||
${config.icon
|
||||
? html`<ha-icon .icon=${config.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${config.text
|
||||
? html`<span class="text">${config.text}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-control-button {
|
||||
--control-button-border-radius: var(
|
||||
--ha-heading-badge-border-radius,
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
--control-button-padding: 0;
|
||||
--mdc-icon-size: var(--ha-heading-badge-icon-size, 14px);
|
||||
width: auto;
|
||||
height: var(--ha-heading-badge-size, 26px);
|
||||
min-width: var(--ha-heading-badge-size, 26px);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
ha-control-button.with-text {
|
||||
--control-button-padding: 0 var(--ha-space-2);
|
||||
}
|
||||
ha-control-button.colored {
|
||||
--control-button-icon-color: var(--color);
|
||||
--control-button-background-color: var(--color);
|
||||
--control-button-focus-color: var(--color);
|
||||
--ha-ripple-color: var(--color);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.text {
|
||||
padding: 0 var(--ha-space-1);
|
||||
line-height: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-button-heading-badge": HuiButtonHeadingBadge;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import "../../../state-display/state-display";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
|
||||
@@ -44,24 +43,6 @@ export class HuiEntityHeadingBadge
|
||||
return document.createElement("hui-heading-entity-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(hass: HomeAssistant): EntityHeadingBadgeConfig {
|
||||
const includeDomains = ["sensor", "light", "switch"];
|
||||
const maxEntities = 1;
|
||||
const entities = Object.keys(hass.states);
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
[],
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
type: "entity",
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EntityHeadingBadgeConfig;
|
||||
|
||||
@@ -26,13 +26,3 @@ export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface ButtonHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||
type: "button";
|
||||
text?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,11 @@ import {
|
||||
HOME_SUMMARIES_ICONS,
|
||||
} from "./helpers/home-summaries";
|
||||
import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy";
|
||||
import type { HomeOtherDevicesViewStrategyConfig } from "./home-other-devices-view-strategy";
|
||||
import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy";
|
||||
|
||||
export interface HomeDashboardStrategyConfig {
|
||||
type: "home";
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
}
|
||||
|
||||
@customElement("home-dashboard-strategy")
|
||||
@@ -79,8 +77,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
subview: true,
|
||||
strategy: {
|
||||
type: "home-other-devices",
|
||||
home_panel: config.home_panel,
|
||||
} satisfies HomeOtherDevicesViewStrategyConfig,
|
||||
},
|
||||
icon: "mdi:devices",
|
||||
} satisfies LovelaceViewRawConfig;
|
||||
|
||||
|
||||
@@ -12,21 +12,17 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isHelperDomain } from "../../../config/helpers/const";
|
||||
import type {
|
||||
EmptyStateCardConfig,
|
||||
HeadingCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
|
||||
|
||||
export interface HomeOtherDevicesViewStrategyConfig {
|
||||
type: "home-other-devices";
|
||||
home_panel?: boolean;
|
||||
}
|
||||
|
||||
@customElement("home-other-devices-view-strategy")
|
||||
export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
config: HomeOtherDevicesViewStrategyConfig,
|
||||
_config: HomeOtherDevicesViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const allEntities = Object.keys(hass.states);
|
||||
@@ -136,24 +132,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
action: "more-info",
|
||||
},
|
||||
})),
|
||||
...(config.home_panel && device && hass.user?.is_admin
|
||||
? [
|
||||
{
|
||||
type: "button",
|
||||
icon: "mdi:home-plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.assign_area"
|
||||
),
|
||||
tap_action: {
|
||||
action: "fire-dom-event",
|
||||
home_panel: {
|
||||
type: "assign_area",
|
||||
device_id: device.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
} satisfies HeadingCardConfig,
|
||||
...entities.map((e) => ({
|
||||
@@ -208,26 +186,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
// No sections, show empty state
|
||||
if (sections.length === 0) {
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:check-all",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.empty_state_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.empty_state_content"
|
||||
),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Take the full width if there is only one section to avoid narrow header on desktop
|
||||
if (sections.length === 1) {
|
||||
sections[0].column_span = 2;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
mdiChevronDown,
|
||||
mdiMonitor,
|
||||
@@ -18,16 +20,11 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import {
|
||||
startMediaProgressInterval,
|
||||
stopMediaProgressInterval,
|
||||
} from "../../common/util/media-progress";
|
||||
import { VolumeSliderController } from "../../common/util/volume-slider";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-button-menu";
|
||||
import "../../components/ha-domain-icon";
|
||||
import "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-slider";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-state-icon";
|
||||
@@ -53,7 +50,6 @@ import type { ResolvedMediaSource } from "../../data/media_source";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaSlider } from "../../components/ha-slider";
|
||||
import "../lovelace/components/hui-marquee";
|
||||
import {
|
||||
BrowserMediaPlayer,
|
||||
@@ -74,40 +70,20 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@query(".progress-slider") private _progressBar?: HaSlider;
|
||||
@query("mwc-linear-progress") private _progressBar?: LinearProgress;
|
||||
|
||||
@query("#CurrentProgress") private _currentProgress?: HTMLElement;
|
||||
|
||||
@query(".volume-slider") private _volumeSlider?: HaSlider;
|
||||
|
||||
@state() private _marqueeActive = false;
|
||||
|
||||
@state() private _newMediaExpected = false;
|
||||
|
||||
@state() private _browserPlayer?: BrowserMediaPlayer;
|
||||
|
||||
private _volumeValue = 0;
|
||||
|
||||
private _progressInterval?: number;
|
||||
|
||||
private _browserPlayerVolume = 0.8;
|
||||
|
||||
private _volumeStep = 2;
|
||||
|
||||
private _debouncedVolumeSet = debounce((value: number) => {
|
||||
this._setVolume(value);
|
||||
}, 100);
|
||||
|
||||
private _volumeController = new VolumeSliderController({
|
||||
getSlider: () => this._volumeSlider,
|
||||
step: this._volumeStep,
|
||||
onSetVolume: (value) => this._setVolume(value),
|
||||
onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value),
|
||||
onValueUpdated: (value) => {
|
||||
this._volumeValue = value;
|
||||
},
|
||||
});
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -118,20 +94,23 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (
|
||||
!this._progressInterval &&
|
||||
this._showProgressBar &&
|
||||
stateObj.state === "playing" &&
|
||||
!this._progressInterval
|
||||
stateObj.state === "playing"
|
||||
) {
|
||||
this._progressInterval = startMediaProgressInterval(
|
||||
this._progressInterval,
|
||||
() => this._updateProgressBar()
|
||||
this._progressInterval = window.setInterval(
|
||||
() => this._updateProgressBar(),
|
||||
1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._progressInterval = stopMediaProgressInterval(this._progressInterval);
|
||||
if (this._progressInterval) {
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
}
|
||||
this._tearDownBrowserPlayer();
|
||||
}
|
||||
|
||||
@@ -195,7 +174,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return this._renderChoosePlayer(stateObj, this._volumeValue);
|
||||
return this._renderChoosePlayer(stateObj);
|
||||
}
|
||||
|
||||
const controls: ControlButton[] | undefined = !this.narrow
|
||||
@@ -235,6 +214,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
const mediaArt =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -291,55 +271,21 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
${stateObj.attributes.media_duration === Infinity
|
||||
? nothing
|
||||
: this.narrow
|
||||
? html`<ha-slider
|
||||
class="progress-slider"
|
||||
min="0"
|
||||
max=${stateObj.attributes.media_duration || 0}
|
||||
step="1"
|
||||
.value=${getCurrentProgress(stateObj)}
|
||||
.withTooltip=${false}
|
||||
size="small"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
?disabled=${isBrowser ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
></ha-slider>`
|
||||
? html`<mwc-linear-progress></mwc-linear-progress>`
|
||||
: html`
|
||||
<div class="progress">
|
||||
<div id="CurrentProgress"></div>
|
||||
<ha-slider
|
||||
class="progress-slider"
|
||||
min="0"
|
||||
max=${stateObj.attributes.media_duration || 0}
|
||||
step="1"
|
||||
.value=${getCurrentProgress(stateObj)}
|
||||
.withTooltip=${false}
|
||||
size="small"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.card.media_player.track_position"
|
||||
)}
|
||||
?disabled=${isBrowser ||
|
||||
!supportsFeature(
|
||||
stateObj,
|
||||
MediaPlayerEntityFeature.SEEK
|
||||
)}
|
||||
@change=${this._handleMediaSeekChanged}
|
||||
></ha-slider>
|
||||
<mwc-linear-progress wide></mwc-linear-progress>
|
||||
<div>${mediaDuration}</div>
|
||||
</div>
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
${this._renderChoosePlayer(stateObj, this._volumeValue)}
|
||||
${this._renderChoosePlayer(stateObj)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderChoosePlayer(
|
||||
stateObj: MediaPlayerEntity | undefined,
|
||||
volumeValue: number
|
||||
) {
|
||||
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
|
||||
const isBrowser = this.entityId === BROWSER_PLAYER;
|
||||
return html`
|
||||
<div class="choose-player ${isBrowser ? "browser" : ""}">
|
||||
@@ -348,42 +294,26 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
|
||||
? html`
|
||||
<ha-dropdown class="volume-menu" placement="top" .distance=${8}>
|
||||
<ha-button-menu y="0" x="76">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiVolumeHigh}
|
||||
></ha-icon-button>
|
||||
<div
|
||||
class="volume-slider-container"
|
||||
@touchstart=${this._volumeController.handleTouchStart}
|
||||
@touchmove=${this._volumeController.handleTouchMove}
|
||||
@touchend=${this._volumeController.handleTouchEnd}
|
||||
@touchcancel=${this._volumeController.handleTouchCancel}
|
||||
@wheel=${this._volumeController.handleWheel}
|
||||
<ha-slider
|
||||
labeled
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
.value=${stateObj.attributes.volume_level! * 100}
|
||||
@change=${this._handleVolumeChange}
|
||||
>
|
||||
<ha-slider
|
||||
class="volume-slider"
|
||||
labeled
|
||||
min="0"
|
||||
max="100"
|
||||
.step=${this._volumeStep}
|
||||
.value=${volumeValue}
|
||||
@input=${this._volumeController.handleInput}
|
||||
@change=${this._volumeController.handleChange}
|
||||
>
|
||||
</ha-slider>
|
||||
</div>
|
||||
</ha-dropdown>
|
||||
</ha-slider>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<ha-dropdown
|
||||
class="player-menu"
|
||||
placement="top-end"
|
||||
.distance=${8}
|
||||
@wa-select=${this._handlePlayerSelect}
|
||||
>
|
||||
<ha-button-menu>
|
||||
${
|
||||
this.narrow
|
||||
? html`
|
||||
@@ -412,24 +342,26 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
<ha-dropdown-item
|
||||
class=${isBrowser ? "selected" : ""}
|
||||
.value=${BROWSER_PLAYER}
|
||||
<ha-list-item
|
||||
.player=${BROWSER_PLAYER}
|
||||
?selected=${isBrowser}
|
||||
@click=${this._selectPlayer}
|
||||
>
|
||||
${this.hass.localize("ui.components.media-browser.web-browser")}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
${this._mediaPlayerEntities.map(
|
||||
(source) => html`
|
||||
<ha-dropdown-item
|
||||
class=${source.entity_id === this.entityId ? "selected" : ""}
|
||||
<ha-list-item
|
||||
?selected=${source.entity_id === this.entityId}
|
||||
.disabled=${source.state === UNAVAILABLE}
|
||||
.value=${source.entity_id}
|
||||
.player=${source.entity_id}
|
||||
@click=${this._selectPlayer}
|
||||
>
|
||||
${computeStateName(source)}
|
||||
</ha-dropdown-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -469,9 +401,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
this._newMediaExpected = false;
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
this._updateVolumeValueFromState(this._stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -490,25 +419,23 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (this.entityId === BROWSER_PLAYER) {
|
||||
this._updateVolumeValueFromState(stateObj);
|
||||
}
|
||||
|
||||
this._updateProgressBar();
|
||||
this._syncVolumeSlider();
|
||||
|
||||
if (this._showProgressBar && stateObj?.state === "playing") {
|
||||
this._progressInterval = startMediaProgressInterval(
|
||||
this._progressInterval,
|
||||
() => this._updateProgressBar()
|
||||
if (
|
||||
!this._progressInterval &&
|
||||
this._showProgressBar &&
|
||||
stateObj?.state === "playing"
|
||||
) {
|
||||
this._progressInterval = window.setInterval(
|
||||
() => this._updateProgressBar(),
|
||||
1000
|
||||
);
|
||||
} else if (
|
||||
this._progressInterval &&
|
||||
(!this._showProgressBar || stateObj?.state !== "playing")
|
||||
) {
|
||||
this._progressInterval = stopMediaProgressInterval(
|
||||
this._progressInterval
|
||||
);
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,45 +489,25 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
private _updateProgressBar(): void {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!this._progressBar || !stateObj) {
|
||||
if (!this._progressBar || !this._currentProgress || !stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stateObj.attributes.media_duration) {
|
||||
this._progressBar.value = 0;
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = "";
|
||||
}
|
||||
this._progressBar.progress = 0;
|
||||
this._currentProgress.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgress = getCurrentProgress(stateObj);
|
||||
this._progressBar.max = stateObj.attributes.media_duration;
|
||||
this._progressBar.value = currentProgress;
|
||||
this._progressBar.progress =
|
||||
currentProgress / stateObj.attributes.media_duration;
|
||||
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = formatMediaTime(currentProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateVolumeValueFromState(stateObj?: MediaPlayerEntity): void {
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
const volumeLevel = stateObj.attributes.volume_level;
|
||||
if (typeof volumeLevel !== "number" || !Number.isFinite(volumeLevel)) {
|
||||
return;
|
||||
}
|
||||
this._volumeValue = Math.round(volumeLevel * 100);
|
||||
}
|
||||
|
||||
private _syncVolumeSlider(): void {
|
||||
if (!this._volumeSlider || this._volumeController.isInteracting) {
|
||||
return;
|
||||
}
|
||||
this._volumeSlider.value = this._volumeValue;
|
||||
}
|
||||
|
||||
private _handleControlClick(e: MouseEvent): void {
|
||||
const action = (e.currentTarget! as HTMLElement).getAttribute("action")!;
|
||||
|
||||
@@ -619,18 +526,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMediaSeekChanged(e: Event): void {
|
||||
if (this.entityId === BROWSER_PLAYER || !this._stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = (e.target as HaSlider).value;
|
||||
this.hass.callService("media_player", "media_seek", {
|
||||
entity_id: this._stateObj.entity_id,
|
||||
seek_position: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _marqueeMouseOver(): void {
|
||||
if (!this._marqueeActive) {
|
||||
this._marqueeActive = true;
|
||||
@@ -643,19 +538,20 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePlayerSelect(ev: CustomEvent): void {
|
||||
const entityId = (ev.detail.item as any).value;
|
||||
private _selectPlayer(ev: CustomEvent): void {
|
||||
const entityId = (ev.currentTarget as any).player;
|
||||
fireEvent(this, "player-picked", { entityId });
|
||||
}
|
||||
|
||||
private _setVolume(value: number) {
|
||||
const volume = value / 100;
|
||||
private async _handleVolumeChange(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = Number(ev.target.value) / 100;
|
||||
if (this._browserPlayer) {
|
||||
this._browserPlayerVolume = volume;
|
||||
this._browserPlayer.setVolume(volume);
|
||||
return;
|
||||
this._browserPlayerVolume = value;
|
||||
this._browserPlayer.setVolume(value);
|
||||
} else {
|
||||
await setMediaPlayerVolume(this.hass, this.entityId, value);
|
||||
}
|
||||
setMediaPlayerVolume(this.hass, this.entityId, volume);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -674,11 +570,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
margin-left: var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
ha-slider {
|
||||
mwc-linear-progress {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
--ha-slider-thumb-color: var(--primary-color);
|
||||
--ha-slider-indicator-color: var(--primary-color);
|
||||
padding: 0 4px;
|
||||
--mdc-theme-primary: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-button-menu ha-button[slot="trigger"] {
|
||||
@@ -716,7 +611,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -739,35 +633,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress > div:first-child {
|
||||
margin-right: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.progress > div:last-child {
|
||||
margin-left: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.progress ha-slider {
|
||||
mwc-linear-progress[wide] {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
ha-dropdown.volume-menu::part(menu) {
|
||||
width: 220px;
|
||||
max-width: 220px;
|
||||
overflow: visible;
|
||||
padding: 15px 15px;
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.volume-slider {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.media-info {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -831,14 +700,14 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-slider {
|
||||
:host([narrow]) mwc-linear-progress {
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
top: -4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
ha-dropdown-item.selected {
|
||||
ha-list-item[selected] {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -7278,8 +7278,9 @@
|
||||
"title": "Devices",
|
||||
"description": "Generic information about your devices.",
|
||||
"header": "Device analytics",
|
||||
"info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
|
||||
"learn_more": "Learn more about the device database and how we process your data",
|
||||
"info": "Anonymously share data about your devices to help build the Open Home Foundation's device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign). Learn more about the device database and how we process your data in our",
|
||||
"data_use_statement": "Data Use Statement",
|
||||
"data_use_statement_suffix": ", which you accept by opting in.",
|
||||
"alert": {
|
||||
"title": "Important",
|
||||
"content": "Only enable this option if you understand that your device information will be shared."
|
||||
@@ -7582,10 +7583,7 @@
|
||||
},
|
||||
"other_devices": {
|
||||
"helpers": "Helpers",
|
||||
"entities": "Entities",
|
||||
"assign_area": "Assign area",
|
||||
"empty_state_title": "All devices are organized",
|
||||
"empty_state_content": "There are no unassigned devices left. All devices are organized into areas."
|
||||
"entities": "Entities"
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
@@ -8483,7 +8481,7 @@
|
||||
"title": "Title",
|
||||
"subtitle": "Subtitle"
|
||||
},
|
||||
"badges": "Badges",
|
||||
"entities": "Entities",
|
||||
"entity_config": {
|
||||
"color": "[%key:ui::panel::lovelace::editor::card::tile::color%]",
|
||||
"color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]",
|
||||
@@ -8498,12 +8496,6 @@
|
||||
"state": "[%key:ui::panel::lovelace::editor::badge::entity::displayed_elements_options::state%]"
|
||||
}
|
||||
},
|
||||
"button_config": {
|
||||
"text": "Text",
|
||||
"color": "Color",
|
||||
"visibility": "Visibility",
|
||||
"visibility_explanation": "The button will be shown when ALL conditions below are fulfilled. If no conditions are set, the button will always be shown."
|
||||
},
|
||||
"default_heading": "Kitchen"
|
||||
},
|
||||
"map": {
|
||||
@@ -8781,11 +8773,6 @@
|
||||
"remove": "Remove entity",
|
||||
"form-label": "Edit entity"
|
||||
},
|
||||
"badges": {
|
||||
"name": "Badges",
|
||||
"edit": "Edit badge",
|
||||
"remove": "Remove badge"
|
||||
},
|
||||
"features": {
|
||||
"name": "Features",
|
||||
"not_compatible": "Not compatible",
|
||||
@@ -9040,19 +9027,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"heading-badges": {
|
||||
"add": "Add badge",
|
||||
"no_entity": "No entity selected",
|
||||
"entity_not_found": "Entity not found",
|
||||
"types": {
|
||||
"entity": {
|
||||
"label": "Entity"
|
||||
},
|
||||
"button": {
|
||||
"label": "Button"
|
||||
}
|
||||
}
|
||||
},
|
||||
"strategy": {
|
||||
"original-states": {
|
||||
"areas": "Areas to display",
|
||||
|
||||
Reference in New Issue
Block a user