mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 21:36:36 +00:00
View background settings (#23133)
Co-authored-by: karwosts <karwosts@gmail.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
53caef8f92
commit
7900eb4054
@ -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}
|
||||
|
98
src/components/ha-selector/ha-selector-button-toggle.ts
Normal file
98
src/components/ha-selector/ha-selector-button-toggle.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
`;
|
||||
|
130
src/panels/lovelace/views/hui-view-background.ts
Normal file
130
src/panels/lovelace/views/hui-view-background.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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))
|
||||
);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user