mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-25 02:27:17 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73a1ce90c3 | |||
| ea1b7b9dec | |||
| fd506d4d72 | |||
| a3be09018c | |||
| 3364d4f578 |
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
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?.();
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ export interface PickerComboBoxItem {
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
isRelated?: boolean;
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
|
||||
@@ -423,12 +423,17 @@ export const formatMediaTime = (seconds: number | undefined): string => {
|
||||
return "";
|
||||
}
|
||||
|
||||
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");
|
||||
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)}`;
|
||||
};
|
||||
|
||||
export const cleanupMediaTitle = (title?: string): string | undefined => {
|
||||
|
||||
@@ -14,9 +14,14 @@ 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";
|
||||
@@ -38,6 +43,7 @@ import {
|
||||
cleanupMediaTitle,
|
||||
computeMediaControls,
|
||||
computeMediaDescription,
|
||||
formatMediaTime,
|
||||
handleMediaControlClick,
|
||||
MediaPlayerEntityFeature,
|
||||
mediaPlayerPlayMedia,
|
||||
@@ -54,6 +60,34 @@ 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) =>
|
||||
@@ -62,14 +96,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _formatDuration(duration: number) {
|
||||
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,
|
||||
})!;
|
||||
return formatMediaTime(duration);
|
||||
}
|
||||
|
||||
protected _renderVolumeControl() {
|
||||
@@ -139,13 +166,25 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
${!supportsMute
|
||||
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
<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>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -261,17 +300,17 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
|
||||
const stateObj = this.stateObj;
|
||||
const controls = computeMediaControls(stateObj, true);
|
||||
const coverUrl =
|
||||
const coverUrlRaw =
|
||||
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");
|
||||
@@ -331,8 +370,12 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
?disabled=${!stateActive(stateObj) ||
|
||||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
|
||||
>
|
||||
<span slot="reference">${positionFormatted}</span>
|
||||
<span slot="reference">${remainingFormatted}</span>
|
||||
<span class="position-time" slot="reference"
|
||||
>${positionFormatted}</span
|
||||
>
|
||||
<span class="position-time" slot="reference"
|
||||
>${durationFormatted}</span
|
||||
>
|
||||
</ha-slider>
|
||||
</div>
|
||||
`
|
||||
@@ -531,6 +574,16 @@ 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;
|
||||
@@ -568,6 +621,10 @@ 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;
|
||||
@@ -622,6 +679,39 @@ 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,
|
||||
@@ -629,10 +719,10 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _selectedValueChanged(e: Event): void {
|
||||
private _setVolume(value: number) {
|
||||
this.hass!.callService("media_player", "volume_set", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
volume_level: (e.target as any).value / 100,
|
||||
volume_level: value / 100,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._childView = undefined;
|
||||
this.large = false;
|
||||
this._setQuickBarContext();
|
||||
this._loadEntityRegistryEntry();
|
||||
}
|
||||
|
||||
@@ -164,8 +163,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
} catch (_e) {
|
||||
this._entry = null;
|
||||
}
|
||||
|
||||
this._setQuickBarContext();
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@@ -218,53 +215,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
return !!this.hass.auth.external?.config.hasEntityAddTo;
|
||||
}
|
||||
|
||||
private _setQuickBarContext(): void {
|
||||
if (!this._entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._entityId] as HassEntity | undefined;
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? getEntityEntryContext(
|
||||
this._entry,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const domain = computeDomain(this._entityId);
|
||||
let itemType: ItemType = SearchableDomains.has(domain)
|
||||
? (domain as ItemType)
|
||||
: "entity";
|
||||
let itemId = this._entityId;
|
||||
|
||||
if (context?.device) {
|
||||
itemType = "device";
|
||||
itemId = context.device.id;
|
||||
} else if (context?.area) {
|
||||
itemType = "area";
|
||||
itemId = context.area.area_id;
|
||||
} else if (context?.floor) {
|
||||
itemType = "floor";
|
||||
itemId = context.floor.floor_id;
|
||||
}
|
||||
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType,
|
||||
itemId,
|
||||
});
|
||||
}
|
||||
|
||||
private _getDeviceId(): string | null {
|
||||
const entity = this.hass.entities[this._entityId!] as
|
||||
| EntityRegistryEntry
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
type ActionCommandComboBoxItem,
|
||||
type NavigationComboBoxItem,
|
||||
} from "../../data/quick_bar";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -54,11 +52,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isIosApp } from "../../util/is_ios";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||
import type {
|
||||
QuickBarContextItem,
|
||||
QuickBarParams,
|
||||
QuickBarSection,
|
||||
} from "./show-dialog-quick-bar";
|
||||
import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar";
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
@@ -76,10 +70,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _relatedResult?: RelatedResult;
|
||||
|
||||
@state() private _contextItem?: QuickBarContextItem;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
private get _showEntityId() {
|
||||
@@ -101,15 +91,6 @@ export class QuickBar extends LitElement {
|
||||
this._initialize();
|
||||
this._selectedSection = params.mode;
|
||||
this._hint = params.hint;
|
||||
|
||||
this._contextItem = params.contextItem;
|
||||
this._relatedResult = params.related;
|
||||
|
||||
if (!params.contextItem) {
|
||||
this._contextItem = undefined;
|
||||
this._relatedResult = undefined;
|
||||
}
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -396,8 +377,6 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
this._configEntryLookup,
|
||||
this._contextItem,
|
||||
this._relatedResult,
|
||||
searchString,
|
||||
this._selectedSection
|
||||
);
|
||||
@@ -406,13 +385,10 @@ export class QuickBar extends LitElement {
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
contextItem: QuickBarContextItem | undefined,
|
||||
relatedResult: RelatedResult | undefined,
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const relatedIdSets = this._getRelatedIdSets(contextItem, relatedResult);
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -460,29 +436,17 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (!section || section === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.entities.size > 0) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
let entityItems = this._getEntitiesMemoized(this.hass).sort(
|
||||
this._sortBySortingLabel
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
entityItems = this._sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[]
|
||||
);
|
||||
} else {
|
||||
entityItems = this._sortRelatedByLabel(entityItems);
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
filter,
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!section && entityItems.length) {
|
||||
@@ -499,25 +463,15 @@ export class QuickBar extends LitElement {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup
|
||||
);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.devices.size > 0) {
|
||||
deviceItems = deviceItems.map((item) => {
|
||||
const deviceId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(deviceId || ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
).sort(this._sortBySortingLabel);
|
||||
|
||||
if (filter) {
|
||||
deviceItems = this._sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
filter,
|
||||
deviceComboBoxKeys
|
||||
);
|
||||
} else {
|
||||
deviceItems = this._sortRelatedByLabel(deviceItems);
|
||||
}
|
||||
|
||||
if (!section && deviceItems.length) {
|
||||
@@ -533,23 +487,13 @@ export class QuickBar extends LitElement {
|
||||
if (this.hass.user?.is_admin && (!section || section === "area")) {
|
||||
let areaItems = this._getAreasMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.areas.size > 0) {
|
||||
areaItems = areaItems.map((item) => {
|
||||
const areaId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
...item,
|
||||
isRelated: relatedIdSets.areas.has(areaId || ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
areaItems = this._sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
areaItems = this._filterGroup(
|
||||
"area",
|
||||
areaItems,
|
||||
filter,
|
||||
areaComboBoxKeys
|
||||
);
|
||||
} else {
|
||||
areaItems = this._sortRelatedByLabel(areaItems);
|
||||
}
|
||||
|
||||
if (!section && areaItems.length) {
|
||||
@@ -566,108 +510,6 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getRelatedIdSets = memoizeOne(
|
||||
(contextItem?: QuickBarContextItem, related?: RelatedResult) => {
|
||||
const entities = new Set(related?.entity || []);
|
||||
const devices = new Set(related?.device || []);
|
||||
const areas = new Set(related?.area || []);
|
||||
|
||||
if (contextItem) {
|
||||
switch (contextItem.itemType) {
|
||||
case "domain":
|
||||
this._getDomainEntities(contextItem.itemId).forEach((entityId) =>
|
||||
entities.add(entityId)
|
||||
);
|
||||
break;
|
||||
case "entity":
|
||||
entities.add(contextItem.itemId);
|
||||
break;
|
||||
case "device":
|
||||
devices.add(contextItem.itemId);
|
||||
break;
|
||||
case "area":
|
||||
areas.add(contextItem.itemId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { entities, devices, areas };
|
||||
}
|
||||
);
|
||||
|
||||
private _getDomainEntities(domain: string): string[] {
|
||||
const states = Object.values(this.hass.states);
|
||||
if (domain === "energy") {
|
||||
return states
|
||||
.filter((stateObj) => {
|
||||
if (computeDomain(stateObj.entity_id) !== "sensor") {
|
||||
return false;
|
||||
}
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
return (
|
||||
typeof deviceClass === "string" &&
|
||||
["energy", "power", "gas", "water"].includes(deviceClass)
|
||||
);
|
||||
})
|
||||
.map((stateObj) => stateObj.entity_id);
|
||||
}
|
||||
|
||||
if (domain === "security") {
|
||||
const coverClasses = new Set(["door", "garage", "gate"]);
|
||||
const binarySensorClasses = new Set([
|
||||
"lock",
|
||||
"door",
|
||||
"window",
|
||||
"garage_door",
|
||||
"opening",
|
||||
"carbon_monoxide",
|
||||
"gas",
|
||||
"moisture",
|
||||
"safety",
|
||||
"smoke",
|
||||
]);
|
||||
|
||||
return states
|
||||
.filter((stateObj) => {
|
||||
const stateDomain = computeDomain(stateObj.entity_id);
|
||||
if (
|
||||
stateDomain === "camera" ||
|
||||
stateDomain === "alarm_control_panel" ||
|
||||
stateDomain === "lock"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
if (stateDomain === "cover") {
|
||||
return (
|
||||
typeof deviceClass === "string" && coverClasses.has(deviceClass)
|
||||
);
|
||||
}
|
||||
|
||||
if (stateDomain === "binary_sensor") {
|
||||
if (typeof deviceClass !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (binarySensorClasses.has(deviceClass)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
deviceClass === "tamper" &&
|
||||
stateObj.attributes.entity_category === "diagnostic"
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((stateObj) => stateObj.entity_id);
|
||||
}
|
||||
|
||||
return states
|
||||
.filter((stateObj) => computeDomain(stateObj.entity_id) === domain)
|
||||
.map((stateObj) => stateObj.entity_id);
|
||||
}
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
getEntities(
|
||||
hass,
|
||||
@@ -767,23 +609,6 @@ export class QuickBar extends LitElement {
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
private _sortRelatedByLabel = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
if (a.isRelated && !b.isRelated) return -1;
|
||||
if (!a.isRelated && b.isRelated) return 1;
|
||||
return this._sortBySortingLabel(a, b);
|
||||
});
|
||||
|
||||
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
|
||||
// #endregion data
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
|
||||
export type QuickBarSection =
|
||||
| "entity"
|
||||
@@ -8,19 +7,10 @@ export type QuickBarSection =
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export type QuickBarContextItemType = ItemType | "domain";
|
||||
|
||||
export interface QuickBarContextItem {
|
||||
itemType: QuickBarContextItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
hint?: string;
|
||||
contextItem?: QuickBarContextItem;
|
||||
related?: RelatedResult;
|
||||
}
|
||||
|
||||
export const loadQuickBar = () => import("./ha-quick-bar");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -46,8 +45,6 @@ class PanelClimate extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setQuickBarContext();
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
@@ -79,7 +76,6 @@ class PanelClimate extends LitElement {
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setQuickBarContext();
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
@@ -167,13 +163,6 @@ class PanelClimate extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _setQuickBarContext() {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "domain",
|
||||
itemId: "climate",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -80,12 +80,6 @@ class DialogAreaDetail
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "area",
|
||||
itemId: this._params.entry.area_id,
|
||||
});
|
||||
}
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name;
|
||||
this._aliases = this._params.entry.aliases;
|
||||
|
||||
@@ -55,12 +55,6 @@ class DialogFloorDetail extends LitElement {
|
||||
public showDialog(params: FloorRegistryDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "floor",
|
||||
itemId: this._params.entry.floor_id,
|
||||
});
|
||||
}
|
||||
this._name = this._params.entry
|
||||
? this._params.entry.name
|
||||
: this._params.suggestedName || "";
|
||||
|
||||
@@ -10,7 +10,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
@@ -138,12 +137,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("areaId")) {
|
||||
this._findRelated();
|
||||
if (this.areaId) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "area",
|
||||
itemId: this.areaId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -682,16 +682,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
sub(this._config)
|
||||
);
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityId") || changedProps.has("entityId")) {
|
||||
const automationEntityId = this._entityId ?? this.entityId;
|
||||
if (automationEntityId) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "automation",
|
||||
itemId: automationEntityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _setEntityId() {
|
||||
|
||||
@@ -41,10 +41,6 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "device",
|
||||
itemId: this._params.device.id,
|
||||
});
|
||||
this._nameByUser = this._params.device.name_by_user || "";
|
||||
this._areaId = this._params.device.area_id || "";
|
||||
this._labels = this._params.device.labels || [];
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
|
||||
@@ -303,11 +302,6 @@ export class HaConfigDevicePage extends LitElement {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._findRelated();
|
||||
// Broadcast device context for quick bar
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "device",
|
||||
itemId: this.deviceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,10 +105,19 @@ class AddIntegrationDialog extends LitElement {
|
||||
const loadPromise = this._load();
|
||||
|
||||
if (params?.domain) {
|
||||
// Just open the config flow dialog, do not show this dialog
|
||||
// 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
|
||||
await loadPromise;
|
||||
await this._createFlow(params.domain);
|
||||
return;
|
||||
const flowsInProgress = this._getFlowsInProgressForDomains([
|
||||
params.domain,
|
||||
]);
|
||||
|
||||
if (!flowsInProgress.length) {
|
||||
await this._createFlow(params.domain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (params?.brand === "_discovered") {
|
||||
@@ -117,10 +126,12 @@ class AddIntegrationDialog extends LitElement {
|
||||
this._showDiscovered = true;
|
||||
}
|
||||
|
||||
// Only open the dialog if no domain is provided
|
||||
// Only open the dialog if no domain is provided or we need to select a flow
|
||||
this._open = true;
|
||||
this._pickedBrand =
|
||||
params?.brand === "_discovered" ? undefined : params?.brand;
|
||||
params?.brand === "_discovered"
|
||||
? undefined
|
||||
: params?.domain || params?.brand;
|
||||
this._initialFilter = params?.initialFilter;
|
||||
this._navigateToResult = params?.navigateToResult ?? false;
|
||||
this._narrow = matchMedia(
|
||||
|
||||
@@ -43,12 +43,6 @@ class DialogLabelDetail
|
||||
public showDialog(params: LabelDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "label",
|
||||
itemId: this._params.entry.label_id,
|
||||
});
|
||||
}
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || "";
|
||||
|
||||
@@ -652,13 +652,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("_scene") && this._scene?.entity_id) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "scene",
|
||||
itemId: this._scene.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
|
||||
@@ -583,13 +583,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._dirty = false;
|
||||
this._readOnly = true;
|
||||
}
|
||||
|
||||
if (changedProps.has("_entityId") && this._entityId) {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "script",
|
||||
itemId: this._entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
|
||||
@@ -22,6 +22,7 @@ export function getAssistantsTableColumn<T>(
|
||||
type: "flex",
|
||||
defaultHidden: !visible,
|
||||
sortable: true,
|
||||
showNarrow: true,
|
||||
minWidth: "160px",
|
||||
maxWidth: "160px",
|
||||
valueColumn: "assistants_sortable_key",
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
@@ -134,7 +133,6 @@ class PanelEnergy extends LitElement {
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setQuickBarContext();
|
||||
this._loadConfig();
|
||||
return;
|
||||
}
|
||||
@@ -143,8 +141,6 @@ class PanelEnergy extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setQuickBarContext();
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
@@ -211,13 +207,6 @@ class PanelEnergy extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _setQuickBarContext() {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "domain",
|
||||
itemId: "energy",
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`
|
||||
|
||||
@@ -3,6 +3,7 @@ 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,
|
||||
@@ -14,6 +15,7 @@ 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")
|
||||
@@ -95,6 +97,29 @@ 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;
|
||||
@@ -107,6 +132,7 @@ class PanelHome extends LitElement {
|
||||
.lovelace=${this._lovelace}
|
||||
.route=${this.route}
|
||||
.panel=${this.panel}
|
||||
@ll-custom=${this._handleLLCustomEvent}
|
||||
></hui-root>
|
||||
`;
|
||||
}
|
||||
@@ -116,6 +142,7 @@ class PanelHome extends LitElement {
|
||||
strategy: {
|
||||
type: "home",
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
home_panel: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -46,8 +45,6 @@ class PanelLight extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setQuickBarContext();
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
@@ -79,7 +76,6 @@ class PanelLight extends LitElement {
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setQuickBarContext();
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
@@ -167,13 +163,6 @@ class PanelLight extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _setQuickBarContext() {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "domain",
|
||||
itemId: "light",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -6,13 +5,12 @@ 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-button-menu";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
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";
|
||||
@@ -94,14 +92,10 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
"ui.panel.lovelace.editor.edit_card.layout.explanation"
|
||||
)}
|
||||
</p>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
.corner=${"BOTTOM_END"}
|
||||
menu-corner="END"
|
||||
@wa-select=${this._handleAction}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -110,13 +104,13 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||
<ha-dropdown-item value="toggle_yaml" .disabled=${!this._uiAvailable}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
${this._yamlMode
|
||||
? html`
|
||||
@@ -244,11 +238,11 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "toggle_yaml") {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +325,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.header ha-button-menu {
|
||||
.header ha-dropdown {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
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,14 +126,11 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition}
|
||||
</h3>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
.corner=${"BOTTOM_END"}
|
||||
menu-corner="END"
|
||||
@wa-select=${this._handleAction}
|
||||
@click=${stopPropagation}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -142,54 +139,50 @@ export class HaCardConditionEditor extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="test">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.test"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="duplicate">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="copy">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="cut">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml"
|
||||
.disabled=${!this._uiAvailable}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-list-item class="warning" graphic="icon">
|
||||
<ha-dropdown-item variant="danger" value="delete">
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${!this._uiAvailable
|
||||
? html`
|
||||
<ha-alert
|
||||
@@ -252,26 +245,31 @@ export class HaCardConditionEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "test":
|
||||
await this._testCondition();
|
||||
break;
|
||||
case 1:
|
||||
return;
|
||||
case "duplicate":
|
||||
this._duplicateCondition();
|
||||
break;
|
||||
case 2:
|
||||
return;
|
||||
case "copy":
|
||||
this._copyCondition();
|
||||
break;
|
||||
case 3:
|
||||
return;
|
||||
case "cut":
|
||||
this._cutCondition();
|
||||
break;
|
||||
case 4:
|
||||
return;
|
||||
case "toggle_yaml":
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case 5:
|
||||
return;
|
||||
case "delete":
|
||||
this._delete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,9 +319,9 @@ export class HaCardConditionEditor extends LitElement {
|
||||
this._delete();
|
||||
}
|
||||
|
||||
private _delete() {
|
||||
private _delete = () => {
|
||||
fireEvent(this, "value-changed", { value: null });
|
||||
}
|
||||
};
|
||||
|
||||
private _onYamlChange(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
@@ -337,7 +335,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
ha-button-menu {
|
||||
ha-dropdown {
|
||||
--mdc-theme-text-primary-on-background: var(--primary-text-color);
|
||||
}
|
||||
ha-expansion-panel {
|
||||
|
||||
@@ -3,11 +3,12 @@ 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-list-item";
|
||||
import type { HaSelect } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
@@ -27,7 +28,6 @@ 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,8 +41,6 @@ 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;
|
||||
@@ -107,11 +105,7 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-button-menu
|
||||
@action=${this._addCondition}
|
||||
fixed
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-dropdown @wa-select=${this._addCondition}>
|
||||
<ha-button slot="trigger" appearance="filled">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
@@ -120,59 +114,57 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
</ha-button>
|
||||
${this._clipboard
|
||||
? html`
|
||||
<ha-list-item .value=${PASTE_VALUE} graphic="icon">
|
||||
<ha-dropdown-item value="paste">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste_condition"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${UI_CONDITION.map(
|
||||
(condition) => html`
|
||||
<ha-list-item .value=${condition} graphic="icon">
|
||||
<ha-dropdown-item .value=${condition}>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition}.label`
|
||||
) || condition}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${ICON_CONDITION[condition]}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addCondition(ev: CustomEvent): void {
|
||||
private _addCondition(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const condition = ev.detail.item.value as "paste" | Condition["condition"];
|
||||
const conditions = [...this.conditions];
|
||||
|
||||
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 });
|
||||
if (!condition || (condition === "paste" && !this._clipboard)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const condition = item.value as Condition["condition"];
|
||||
if (condition === "paste") {
|
||||
const newCondition = deepClone(this._clipboard);
|
||||
conditions.push(newCondition);
|
||||
} else {
|
||||
const elClass = customElements.get(`ha-card-condition-${condition}`) as
|
||||
| LovelaceConditionEditorConstructor
|
||||
| undefined;
|
||||
|
||||
const elClass = customElements.get(`ha-card-condition-${condition}`) as
|
||||
| LovelaceConditionEditorConstructor
|
||||
| undefined;
|
||||
conditions.push(
|
||||
elClass?.defaultConfig ? { ...elClass.defaultConfig } : { condition }
|
||||
);
|
||||
}
|
||||
|
||||
conditions.push(
|
||||
elClass?.defaultConfig
|
||||
? { ...elClass.defaultConfig }
|
||||
: { condition: condition }
|
||||
);
|
||||
this._focusLastConditionOnChange = true;
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
@@ -210,8 +202,9 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
margin-top: 12px;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
ha-button-menu {
|
||||
margin-top: 12px;
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
margin-top: var(--ha-space-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDragHorizontalVariant,
|
||||
@@ -8,10 +9,11 @@ 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";
|
||||
@@ -24,6 +26,7 @@ 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";
|
||||
@@ -52,15 +55,14 @@ 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,
|
||||
@@ -404,45 +406,39 @@ export class HuiCardFeaturesEditor extends LitElement {
|
||||
</ha-sortable>
|
||||
${supportedFeaturesType.length > 0
|
||||
? html`
|
||||
<ha-button-menu
|
||||
fixed
|
||||
@action=${this._addFeature}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-dropdown @wa-select=${this._addFeature}>
|
||||
<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-list-item .value=${type}>
|
||||
<ha-dropdown-item .value=${type}>
|
||||
${this._getFeatureTypeLabel(type)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
${types.length > 0 && customTypes.length > 0
|
||||
? html`<li divider role="separator"></li>`
|
||||
? html`<wa-divider></wa-divider>`
|
||||
: nothing}
|
||||
${customTypes.map(
|
||||
(type) => html`
|
||||
<ha-list-item .value=${type}>
|
||||
<ha-dropdown-item .value=${type}>
|
||||
${this._getFeatureTypeLabel(type)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
private async _addFeature(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const value = ev.detail.item.value as FeatureType;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elClass = await getCardFeatureElementClass(value);
|
||||
|
||||
@@ -499,7 +495,9 @@ export class HuiCardFeaturesEditor extends LitElement {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-button-menu {
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
.feature {
|
||||
|
||||
+24
-28
@@ -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,13 +98,18 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleAction(ev) {
|
||||
ev.stopPropagation();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "toggle-mode":
|
||||
this._toggleMode();
|
||||
break;
|
||||
case 1:
|
||||
case "take-control":
|
||||
this._takeControl();
|
||||
break;
|
||||
}
|
||||
@@ -150,41 +155,32 @@ class DialogDashboardStrategyEditor extends LitElement {
|
||||
${this._params.title
|
||||
? html`<span slot="subtitle">${this._params.title}</span>`
|
||||
: nothing}
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
<ha-dropdown
|
||||
placement="bottom-end"
|
||||
slot="actionItems"
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handleAction}
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-dropdown-item
|
||||
value="toggle-mode"
|
||||
.disabled=${!this._guiModeAvailable && !this._GUImode}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._GUImode ? "ui" : "yaml"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="take-control">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy-editor.take_control"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiAccountHardHat}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiAccountHardHat}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</ha-dialog-header>
|
||||
<div class="content">
|
||||
<hui-dashboard-strategy-element-editor
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDotsVertical,
|
||||
@@ -11,14 +10,15 @@ 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-list-item";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
@@ -218,38 +218,32 @@ export class HuiDialogEditView extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<h2 slot="title">${this._viewConfigTitle}</h2>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
fixed
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
@action=${this._handleAction}
|
||||
@closed=${stopPropagation}
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="toggle-mode">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="move-to-dashboard">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_view.move_to_dashboard"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiFileMoveOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${convertToSection
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
@@ -330,14 +324,18 @@ export class HuiDialogEditView extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "toggle-mode":
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
case 1:
|
||||
case "move-to-dashboard":
|
||||
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-list-item";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
@@ -113,29 +113,23 @@ export class HuiDialogEditViewHeader extends LitElement {
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<h2 slot="title">${title}</h2>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="actionItems"
|
||||
fixed
|
||||
corner="BOTTOM_END"
|
||||
menu-corner="END"
|
||||
@action=${this._handleAction}
|
||||
@closed=${stopPropagation}
|
||||
placement="bottom-end"
|
||||
@wa-select=${this._handleAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass!.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="toggle-mode">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_header.edit_${!this._yamlMode ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</ha-dialog-header>
|
||||
${content}
|
||||
<ha-button
|
||||
@@ -150,13 +144,11 @@ export class HuiDialogEditViewHeader extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._yamlMode = !this._yamlMode;
|
||||
break;
|
||||
private async _handleAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const action = ev.detail.item.value;
|
||||
|
||||
if (action === "toggle-mode") {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import type { QuickBarContextItem } from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -842,34 +841,6 @@ class HUIRoot extends LitElement {
|
||||
fireEvent(this, "config-refresh");
|
||||
}
|
||||
|
||||
private _setQuickBarContext(viewConfig?: LovelaceViewConfig) {
|
||||
if (!this.lovelace || !isStrategyDashboard(this.lovelace.rawConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lovelace.rawConfig.strategy.type !== "home") {
|
||||
return;
|
||||
}
|
||||
|
||||
let contextItem: QuickBarContextItem = {
|
||||
itemType: "entity",
|
||||
itemId: "zone.home",
|
||||
};
|
||||
|
||||
const viewPath = viewConfig?.path;
|
||||
if (viewPath?.startsWith("areas-")) {
|
||||
const areaId = viewPath.slice("areas-".length);
|
||||
if (areaId) {
|
||||
contextItem = {
|
||||
itemType: "area",
|
||||
itemId: areaId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "hass-quick-bar-context", contextItem);
|
||||
}
|
||||
|
||||
private _handleReloadResources(): void {
|
||||
this.hass.callService("lovelace", "reload_resources");
|
||||
showConfirmationDialog(this, {
|
||||
@@ -1242,8 +1213,6 @@ class HUIRoot extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setQuickBarContext(viewConfig);
|
||||
|
||||
if (!force && this._viewCache![viewIndex]) {
|
||||
view = this._viewCache![viewIndex];
|
||||
} else {
|
||||
|
||||
@@ -10,11 +10,13 @@ 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")
|
||||
@@ -77,7 +79,8 @@ 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,17 +12,21 @@ 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 { HeadingCardConfig } from "../../cards/types";
|
||||
import type {
|
||||
EmptyStateCardConfig,
|
||||
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);
|
||||
@@ -132,6 +136,24 @@ 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) => ({
|
||||
@@ -186,6 +208,26 @@ 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,5 +1,3 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
mdiChevronDown,
|
||||
mdiMonitor,
|
||||
@@ -20,11 +18,16 @@ 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";
|
||||
@@ -50,6 +53,7 @@ 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,
|
||||
@@ -70,20 +74,40 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@query("mwc-linear-progress") private _progressBar?: LinearProgress;
|
||||
@query(".progress-slider") private _progressBar?: HaSlider;
|
||||
|
||||
@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();
|
||||
|
||||
@@ -94,23 +118,20 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (
|
||||
!this._progressInterval &&
|
||||
this._showProgressBar &&
|
||||
stateObj.state === "playing"
|
||||
stateObj.state === "playing" &&
|
||||
!this._progressInterval
|
||||
) {
|
||||
this._progressInterval = window.setInterval(
|
||||
() => this._updateProgressBar(),
|
||||
1000
|
||||
this._progressInterval = startMediaProgressInterval(
|
||||
this._progressInterval,
|
||||
() => this._updateProgressBar()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._progressInterval) {
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
}
|
||||
this._progressInterval = stopMediaProgressInterval(this._progressInterval);
|
||||
this._tearDownBrowserPlayer();
|
||||
}
|
||||
|
||||
@@ -174,7 +195,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!stateObj) {
|
||||
return this._renderChoosePlayer(stateObj);
|
||||
return this._renderChoosePlayer(stateObj, this._volumeValue);
|
||||
}
|
||||
|
||||
const controls: ControlButton[] | undefined = !this.narrow
|
||||
@@ -214,7 +235,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
const mediaArt =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -271,21 +291,55 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
${stateObj.attributes.media_duration === Infinity
|
||||
? nothing
|
||||
: this.narrow
|
||||
? html`<mwc-linear-progress></mwc-linear-progress>`
|
||||
? 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`
|
||||
<div class="progress">
|
||||
<div id="CurrentProgress"></div>
|
||||
<mwc-linear-progress wide></mwc-linear-progress>
|
||||
<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>
|
||||
<div>${mediaDuration}</div>
|
||||
</div>
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
${this._renderChoosePlayer(stateObj)}
|
||||
${this._renderChoosePlayer(stateObj, this._volumeValue)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) {
|
||||
private _renderChoosePlayer(
|
||||
stateObj: MediaPlayerEntity | undefined,
|
||||
volumeValue: number
|
||||
) {
|
||||
const isBrowser = this.entityId === BROWSER_PLAYER;
|
||||
return html`
|
||||
<div class="choose-player ${isBrowser ? "browser" : ""}">
|
||||
@@ -294,26 +348,42 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
|
||||
? html`
|
||||
<ha-button-menu y="0" x="76">
|
||||
<ha-dropdown class="volume-menu" placement="top" .distance=${8}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiVolumeHigh}
|
||||
></ha-icon-button>
|
||||
<ha-slider
|
||||
labeled
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
.value=${stateObj.attributes.volume_level! * 100}
|
||||
@change=${this._handleVolumeChange}
|
||||
<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>
|
||||
</ha-button-menu>
|
||||
<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-button-menu>
|
||||
<ha-dropdown
|
||||
class="player-menu"
|
||||
placement="top-end"
|
||||
.distance=${8}
|
||||
@wa-select=${this._handlePlayerSelect}
|
||||
>
|
||||
${
|
||||
this.narrow
|
||||
? html`
|
||||
@@ -342,26 +412,24 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
<ha-list-item
|
||||
.player=${BROWSER_PLAYER}
|
||||
?selected=${isBrowser}
|
||||
@click=${this._selectPlayer}
|
||||
<ha-dropdown-item
|
||||
class=${isBrowser ? "selected" : ""}
|
||||
.value=${BROWSER_PLAYER}
|
||||
>
|
||||
${this.hass.localize("ui.components.media-browser.web-browser")}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
${this._mediaPlayerEntities.map(
|
||||
(source) => html`
|
||||
<ha-list-item
|
||||
?selected=${source.entity_id === this.entityId}
|
||||
<ha-dropdown-item
|
||||
class=${source.entity_id === this.entityId ? "selected" : ""}
|
||||
.disabled=${source.state === UNAVAILABLE}
|
||||
.player=${source.entity_id}
|
||||
@click=${this._selectPlayer}
|
||||
.value=${source.entity_id}
|
||||
>
|
||||
${computeStateName(source)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -401,6 +469,9 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
this._newMediaExpected = false;
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
this._updateVolumeValueFromState(this._stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -419,23 +490,25 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
this._updateProgressBar();
|
||||
if (this.entityId === BROWSER_PLAYER) {
|
||||
this._updateVolumeValueFromState(stateObj);
|
||||
}
|
||||
|
||||
if (
|
||||
!this._progressInterval &&
|
||||
this._showProgressBar &&
|
||||
stateObj?.state === "playing"
|
||||
) {
|
||||
this._progressInterval = window.setInterval(
|
||||
() => this._updateProgressBar(),
|
||||
1000
|
||||
this._updateProgressBar();
|
||||
this._syncVolumeSlider();
|
||||
|
||||
if (this._showProgressBar && stateObj?.state === "playing") {
|
||||
this._progressInterval = startMediaProgressInterval(
|
||||
this._progressInterval,
|
||||
() => this._updateProgressBar()
|
||||
);
|
||||
} else if (
|
||||
this._progressInterval &&
|
||||
(!this._showProgressBar || stateObj?.state !== "playing")
|
||||
) {
|
||||
clearInterval(this._progressInterval);
|
||||
this._progressInterval = undefined;
|
||||
this._progressInterval = stopMediaProgressInterval(
|
||||
this._progressInterval
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,25 +562,45 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
private _updateProgressBar(): void {
|
||||
const stateObj = this._stateObj;
|
||||
|
||||
if (!this._progressBar || !this._currentProgress || !stateObj) {
|
||||
if (!this._progressBar || !stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stateObj.attributes.media_duration) {
|
||||
this._progressBar.progress = 0;
|
||||
this._currentProgress.innerHTML = "";
|
||||
this._progressBar.value = 0;
|
||||
if (this._currentProgress) {
|
||||
this._currentProgress.innerHTML = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProgress = getCurrentProgress(stateObj);
|
||||
this._progressBar.progress =
|
||||
currentProgress / stateObj.attributes.media_duration;
|
||||
this._progressBar.max = stateObj.attributes.media_duration;
|
||||
this._progressBar.value = currentProgress;
|
||||
|
||||
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")!;
|
||||
|
||||
@@ -526,6 +619,18 @@ 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;
|
||||
@@ -538,20 +643,19 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _selectPlayer(ev: CustomEvent): void {
|
||||
const entityId = (ev.currentTarget as any).player;
|
||||
private _handlePlayerSelect(ev: CustomEvent): void {
|
||||
const entityId = (ev.detail.item as any).value;
|
||||
fireEvent(this, "player-picked", { entityId });
|
||||
}
|
||||
|
||||
private async _handleVolumeChange(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = Number(ev.target.value) / 100;
|
||||
private _setVolume(value: number) {
|
||||
const volume = value / 100;
|
||||
if (this._browserPlayer) {
|
||||
this._browserPlayerVolume = value;
|
||||
this._browserPlayer.setVolume(value);
|
||||
} else {
|
||||
await setMediaPlayerVolume(this.hass, this.entityId, value);
|
||||
this._browserPlayerVolume = volume;
|
||||
this._browserPlayer.setVolume(volume);
|
||||
return;
|
||||
}
|
||||
setMediaPlayerVolume(this.hass, this.entityId, volume);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -570,10 +674,11 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
margin-left: var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
mwc-linear-progress {
|
||||
ha-slider {
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
--mdc-theme-primary: var(--secondary-text-color);
|
||||
min-width: 100%;
|
||||
--ha-slider-thumb-color: var(--primary-color);
|
||||
--ha-slider-indicator-color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-button-menu ha-button[slot="trigger"] {
|
||||
@@ -611,6 +716,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -633,10 +739,35 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mwc-linear-progress[wide] {
|
||||
.progress > div:first-child {
|
||||
margin-right: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.progress > div:last-child {
|
||||
margin-left: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.progress ha-slider {
|
||||
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;
|
||||
@@ -700,14 +831,14 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:host([narrow]) mwc-linear-progress {
|
||||
padding: 0;
|
||||
:host([narrow]) ha-slider {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
top: -6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
ha-list-item[selected] {
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -46,8 +45,6 @@ class PanelSecurity extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setQuickBarContext();
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
@@ -79,7 +76,6 @@ class PanelSecurity extends LitElement {
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setQuickBarContext();
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
@@ -167,13 +163,6 @@ class PanelSecurity extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _setQuickBarContext() {
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
itemType: "domain",
|
||||
itemId: "security",
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -6,12 +6,10 @@ import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import type {
|
||||
QuickBarContextItem,
|
||||
QuickBarParams,
|
||||
QuickBarSection,
|
||||
} from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { findRelated, type ItemType, type RelatedResult } from "../data/search";
|
||||
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import type { Redirects } from "../panels/my/ha-panel-my";
|
||||
@@ -25,55 +23,11 @@ declare global {
|
||||
"hass-quick-bar": QuickBarParams;
|
||||
"hass-quick-bar-trigger": KeyboardEvent;
|
||||
"hass-enable-shortcuts": HomeAssistant["enableShortcuts"];
|
||||
"hass-quick-bar-context": QuickBarContextItem | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
class extends superClass {
|
||||
private _quickBarContext?: QuickBarContextItem;
|
||||
|
||||
private _quickBarContextRelated?: RelatedResult;
|
||||
|
||||
private _fetchRelatedMemoized = memoizeOne(
|
||||
(itemType: ItemType, itemId: string) =>
|
||||
findRelated(this.hass!, itemType, itemId)
|
||||
);
|
||||
|
||||
private _clearQuickBarContext = () => {
|
||||
this._quickBarContext = undefined;
|
||||
this._quickBarContextRelated = undefined;
|
||||
};
|
||||
|
||||
private _contextMatches = (context?: QuickBarContextItem) =>
|
||||
context?.itemType === this._quickBarContext?.itemType &&
|
||||
context?.itemId === this._quickBarContext?.itemId;
|
||||
|
||||
private _prefetchQuickBarContext = async (
|
||||
context?: QuickBarContextItem
|
||||
) => {
|
||||
this._quickBarContextRelated = undefined;
|
||||
|
||||
if (!context || context.itemType === "domain") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const related = await this._fetchRelatedMemoized(
|
||||
context.itemType,
|
||||
context.itemId
|
||||
);
|
||||
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = related;
|
||||
}
|
||||
} catch {
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -82,20 +36,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
storeState(this.hass!);
|
||||
});
|
||||
|
||||
this.addEventListener("hass-quick-bar-context", (ev) => {
|
||||
this._quickBarContext =
|
||||
ev.detail && "itemType" in ev.detail && "itemId" in ev.detail
|
||||
? ev.detail
|
||||
: undefined;
|
||||
this._prefetchQuickBarContext(this._quickBarContext);
|
||||
});
|
||||
|
||||
mainWindow.addEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.addEventListener("popstate", this._clearQuickBarContext);
|
||||
|
||||
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
|
||||
switch (ev.detail.key) {
|
||||
case "e":
|
||||
@@ -121,15 +61,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
this._registerShortcut();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mainWindow.removeEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.removeEventListener("popstate", this._clearQuickBarContext);
|
||||
}
|
||||
|
||||
private _registerShortcut() {
|
||||
const shortcutManager = new ShortcutManager();
|
||||
shortcutManager.add({
|
||||
@@ -183,11 +114,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
showQuickBar(this, {
|
||||
mode,
|
||||
contextItem: this._quickBarContext,
|
||||
related: this._quickBarContextRelated,
|
||||
});
|
||||
showQuickBar(this, { mode });
|
||||
}
|
||||
|
||||
private _showShortcutDialog(e: KeyboardEvent) {
|
||||
|
||||
@@ -7582,7 +7582,10 @@
|
||||
},
|
||||
"other_devices": {
|
||||
"helpers": "Helpers",
|
||||
"entities": "Entities"
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
|
||||
Reference in New Issue
Block a user