View background settings (#23133)

Co-authored-by: karwosts <karwosts@gmail.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Simon Lamon 2024-12-22 17:19:54 +01:00 committed by GitHub
parent 53caef8f92
commit 7900eb4054
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 441 additions and 86 deletions

View File

@ -14,6 +14,7 @@ import "../../../../src/panels/lovelace/views/hui-view";
import "../../../../src/panels/lovelace/views/hui-view-container";
import type { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
import "../../../../src/panels/lovelace/views/hui-view-background";
(window as any).loadCardHelpers = () =>
import("../../../../src/panels/lovelace/custom-card-helpers");
@ -57,11 +58,8 @@ class HcLovelace extends LitElement {
const background = viewConfig.background || this.lovelaceConfig.background;
return html`
<hui-view-container
.hass=${this.hass}
.background=${background}
.theme=${viewConfig.theme}
>
<hui-view-container .hass=${this.hass} .theme=${viewConfig.theme}>
<hui-view-background .background=${background}> </hui-view-background>
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}

View File

@ -0,0 +1,98 @@
import "@material/mwc-list/mwc-list-item";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ButtonToggleSelector, SelectOption } from "../../data/selector";
import type { HomeAssistant, ToggleButton } from "../../types";
import "../ha-button-toggle-group";
@customElement("ha-selector-button_toggle")
export class HaButtonToggleSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ButtonToggleSelector;
@property() public value?: string;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: false })
public localizeValue?: (key: string) => string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
protected render() {
const options =
this.selector.button_toggle?.options?.map((option) =>
typeof option === "object"
? (option as SelectOption)
: ({ value: option, label: option } as SelectOption)
) || [];
const translationKey = this.selector.button_toggle?.translation_key;
if (this.localizeValue && translationKey) {
options.forEach((option) => {
const localizedLabel = this.localizeValue!(
`${translationKey}.options.${option.value}`
);
if (localizedLabel) {
option.label = localizedLabel;
}
});
}
if (this.selector.button_toggle?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
const toggleButtons: ToggleButton[] = options.map((item: SelectOption) => ({
label: item.label,
value: item.value,
}));
return html`
${this.label}
<ha-button-toggle-group
.buttons=${toggleButtons}
.active=${this.value}
@value-changed=${this._valueChanged}
></ha-button-toggle-group>
`;
}
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === (this.value ?? "")) {
return;
}
fireEvent(this, "value-changed", {
value: value,
});
}
static styles = css`
:host {
position: relative;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-button_toggle": HaButtonToggleSelector;
}
}

View File

@ -51,6 +51,7 @@ const LOAD_ELEMENTS = {
icon: () => import("./ha-selector-icon"),
media: () => import("./ha-selector-media"),
theme: () => import("./ha-selector-theme"),
button_toggle: () => import("./ha-selector-button-toggle"),
trigger: () => import("./ha-selector-trigger"),
tts: () => import("./ha-selector-tts"),
tts_voice: () => import("./ha-selector-tts-voice"),

View File

@ -7,8 +7,22 @@ export interface ShowViewConfig {
user?: string;
}
interface LovelaceViewBackgroundConfig {
export interface LovelaceViewBackgroundConfig {
image?: string;
transparency?: number;
size?: "auto" | "cover" | "contain";
alignment?:
| "top left"
| "top center"
| "top right"
| "center left"
| "center"
| "center right"
| "bottom left"
| "bottom center"
| "bottom right";
repeat?: "repeat" | "no-repeat";
attachment?: "scroll" | "fixed";
}
export interface LovelaceBaseViewConfig {

View File

@ -26,6 +26,7 @@ export type Selector =
| AreaFilterSelector
| AttributeSelector
| BooleanSelector
| ButtonToggleSelector
| ColorRGBSelector
| ColorTempSelector
| ConditionSelector
@ -108,6 +109,14 @@ export interface BooleanSelector {
boolean: {} | null;
}
export interface ButtonToggleSelector {
button_toggle: {
options: readonly string[] | readonly SelectOption[];
translation_key?: string;
sort?: boolean;
} | null;
}
export interface ColorRGBSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
color_rgb: {} | null;

View File

@ -1,13 +1,14 @@
import "@material/mwc-list/mwc-list-item";
import type { CSSResultGroup } from "lit";
import memoizeOne from "memoize-one";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-selector/ha-selector-image";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
const SELECTOR = { image: { original: true } };
import type { HomeAssistant } from "../../../../types";
@customElement("hui-view-background-editor")
export class HuiViewBackgroundEditor extends LitElement {
@ -19,44 +20,162 @@ export class HuiViewBackgroundEditor extends LitElement {
this._config = config;
}
private _localizeValueCallback = (key: string) =>
this.hass.localize(key as any);
private _schema = memoizeOne((showSettings: boolean) => [
{
name: "image",
selector: { image: { original: true } },
},
...(showSettings
? ([
{
name: "settings",
flatten: true,
expanded: true,
type: "expandable" as const,
schema: [
{
name: "transparency",
selector: {
number: { min: 1, max: 100, mode: "slider" },
},
},
{
name: "attachment",
selector: {
button_toggle: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.attachment",
options: ["scroll", "fixed"],
},
},
},
{
name: "size",
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.size",
options: ["auto", "cover", "contain"],
},
},
},
{
name: "alignment",
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.alignment",
options: [
"top left",
"top center",
"top right",
"center left",
"center",
"center right",
"bottom left",
"bottom center",
"bottom right",
],
},
},
},
{
name: "repeat",
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.repeat",
options: ["repeat", "no-repeat"],
},
},
},
],
},
] as const)
: []),
]);
protected render() {
if (!this.hass) {
return nothing;
}
const background = this._config?.background;
const backgroundUrl =
typeof background === "string"
? background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1]
: background?.image;
let background = this._config?.background;
if (typeof background === "string") {
const backgroundUrl = background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1];
background = {
image: backgroundUrl,
};
}
background = {
transparency: 100,
alignment: "center",
size: "auto",
repeat: "no-repeat",
attachment: "scroll",
...background,
};
return html`
<ha-selector-image
<ha-form
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.title"
)}
.value=${backgroundUrl}
.selector=${SELECTOR}
@value-changed=${this._backgroundChanged}
></ha-selector-image>
.data=${background}
.schema=${this._schema(true)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
.localizeValue=${this._localizeValueCallback}
></ha-form>
`;
}
private _backgroundChanged(ev: ValueChangedEvent<string | null>) {
const backgroundUrl = ev.detail.value;
private _valueChanged(ev: CustomEvent): void {
const config = {
...this._config,
background: {
...(typeof this._config.background === "string"
? {}
: this._config.background),
image: backgroundUrl || undefined,
},
background: ev.detail.value,
};
fireEvent(this, "view-config-changed", { config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "image":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.image"
);
case "transparency":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.transparency"
);
case "alignment":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.alignment.name"
);
case "size":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.size.name"
);
case "repeat":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.repeat.name"
);
case "attachment":
return this.hass.localize(
"ui.panel.lovelace.editor.edit_view.background.attachment.name"
);
default:
return this.hass.localize(
`ui.panel.lovelace.editor.edit_view.background.${schema.name}`
);
}
};
static get styles(): CSSResultGroup {
return css`
:host {

View File

@ -78,6 +78,7 @@ import type { Lovelace } from "./types";
import "./views/hui-view";
import "./views/hui-view-container";
import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
@customElement("hui-root")
class HUIRoot extends LitElement {
@ -469,11 +470,11 @@ class HUIRoot extends LitElement {
</div>
<hui-view-container
.hass=${this.hass}
.background=${background}
.theme=${curViewConfig?.theme}
id="view"
@ll-rebuild=${this._debouncedConfigChanged}
>
<hui-view-background .background=${background}> </hui-view-background>
</hui-view-container>
</div>
`;

View File

@ -0,0 +1,130 @@
import { css, LitElement, nothing } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewBackgroundConfig } from "../../../data/lovelace/config/view";
@customElement("hui-view-background")
export class HUIViewBackground extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) background?:
| string
| LovelaceViewBackgroundConfig
| undefined;
protected render() {
return nothing;
}
private _applyTheme() {
const computedStyles = getComputedStyle(this);
const themeBackground = computedStyles.getPropertyValue(
"--lovelace-background"
);
const fixedBackground = this._isFixedBackground(
this.background || themeBackground
);
const viewBackground = this._computeBackgroundProperty(this.background);
this.toggleAttribute("fixed-background", fixedBackground);
this.style.setProperty("--view-background", viewBackground);
const viewBackgroundOpacity = this._computeBackgroundOpacityProperty(
this.background
);
this.style.setProperty("--view-background-opacity", viewBackgroundOpacity);
}
private _isFixedBackground(
background?: string | LovelaceViewBackgroundConfig
) {
if (typeof background === "string") {
return background.split(" ").includes("fixed");
}
if (typeof background === "object" && background.attachment === "fixed") {
return true;
}
return false;
}
private _computeBackgroundProperty(
background?: string | LovelaceViewBackgroundConfig
) {
if (typeof background === "object" && background.image) {
const size = background.size ?? "auto";
const alignment = background.alignment ?? "center";
const repeat = background.repeat ?? "no-repeat";
return `${alignment} / ${size} ${repeat} url('${background.image}')`;
}
if (typeof background === "string") {
return background;
}
return null;
}
private _computeBackgroundOpacityProperty(
background?: string | LovelaceViewBackgroundConfig
) {
if (typeof background === "object" && background.image) {
if (background.transparency) {
return `${background.transparency}%`;
}
}
return null;
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has("hass") && this.hass) {
const oldHass = changedProperties.get("hass");
if (
!oldHass ||
this.hass.themes !== oldHass.themes ||
this.hass.selectedTheme !== oldHass.selectedTheme
) {
this._applyTheme();
return;
}
}
if (changedProperties.has("background")) {
this._applyTheme();
}
}
static get styles(): CSSResultGroup {
return css`
/* Fixed background hack for Safari iOS */
:host([fixed-background]) {
display: block;
z-index: -1;
position: fixed;
background-attachment: scroll !important;
}
:host(:not([fixed-background])) {
z-index: -1;
position: absolute;
}
:host {
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
width: 100%;
background: var(
--view-background,
var(--lovelace-background, var(--primary-background-color))
);
opacity: var(--view-background-opacity);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-view-background": HUIViewBackground;
}
}

View File

@ -45,23 +45,6 @@ class HuiViewContainer extends LitElement {
);
}
private _isFixedBackground(background?: BackgroundConfig) {
if (typeof background === "string") {
return background.split(" ").includes("fixed");
}
return false;
}
private _computeBackgroundProperty(background?: BackgroundConfig) {
if (typeof background === "object" && background.image) {
return `center / cover no-repeat url('${background.image}')`;
}
if (typeof background === "string") {
return background;
}
return null;
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (changedProperties.has("hass") && this.hass) {
@ -76,7 +59,7 @@ class HuiViewContainer extends LitElement {
}
}
if (changedProperties.has("theme") || changedProperties.has("background")) {
if (changedProperties.has("theme")) {
this._applyTheme();
}
}
@ -89,18 +72,6 @@ class HuiViewContainer extends LitElement {
if (this.hass) {
applyThemesOnElement(this, this.hass?.themes, this.theme);
}
const computedStyles = getComputedStyle(this);
const themeBackground = computedStyles.getPropertyValue(
"--lovelace-background"
);
const fixedBackground = this._isFixedBackground(
this.background || themeBackground
);
const viewBackground = this._computeBackgroundProperty(this.background);
this.toggleAttribute("fixed-background", fixedBackground);
this.style.setProperty("--view-background", viewBackground);
}
static get styles(): CSSResultGroup {
@ -108,30 +79,6 @@ class HuiViewContainer extends LitElement {
:host {
display: relative;
}
/* Fixed background hack for Safari iOS */
:host([fixed-background]) ::slotted(*):before {
display: block;
content: "";
z-index: -1;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
width: 100%;
background: var(
--view-background,
var(--lovelace-background, var(--primary-background-color))
);
background-attachment: scroll !important;
}
:host(:not([fixed-background])) {
background: var(
--view-background,
var(--lovelace-background, var(--primary-background-color))
);
}
`;
}
}

View File

@ -5926,7 +5926,45 @@
"header_name": "{name} View Configuration",
"add": "Add view",
"background": {
"title": "Add a background to the view"
"settings": "Background settings",
"image": "Background image",
"size": {
"name": "Background size",
"options": {
"auto": "Original",
"cover": "Fill view",
"contain": "Fit view"
}
},
"alignment": {
"name": "Background alignment",
"options": {
"top left": "Top left",
"top center": "Top center",
"top right": "Top right",
"center left": "Center left",
"center": "Center",
"center right": "Center right",
"bottom left": "Bottom left",
"bottom center": "Bottom center",
"bottom right": "Bottom right"
}
},
"transparency": "Background transparency",
"repeat": {
"name": "Background repeat",
"options": {
"repeat": "Repeat (tile)",
"no-repeat": "No repeat"
}
},
"attachment": {
"name": "Background attachment",
"options": {
"scroll": "Scroll",
"fixed": "Fixed"
}
}
},
"edit": "Edit view",
"delete": "Delete view",