mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Merge branch 'rc'
This commit is contained in:
commit
7f6ce97199
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250507.0"
|
||||
version = "20250509.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@ -90,7 +90,7 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 24px);
|
||||
padding: 12px 24px max(env(safe-area-inset-bottom), 12px) 24px;
|
||||
}
|
||||
.mdc-dialog__actions span:nth-child(1) {
|
||||
flex: var(--secondary-action-button-flex, unset);
|
||||
@ -107,9 +107,6 @@ export class HaDialog extends DialogBase {
|
||||
.mdc-dialog__title:has(span) {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
padding: 12px 24px 12px 24px;
|
||||
}
|
||||
.mdc-dialog__title::before {
|
||||
content: unset;
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
||||
false;
|
||||
@ -895,6 +895,9 @@ export class HaServiceControl extends LitElement {
|
||||
ha-settings-row {
|
||||
padding: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
@ -916,7 +919,7 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host([hidePicker]) p {
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
|
@ -24,9 +24,10 @@ import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
state,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@ -45,13 +46,13 @@ import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import type { HaMdListItem } from "./ha-md-list-item";
|
||||
import "./ha-menu-button";
|
||||
import "./ha-sortable";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
import "./ha-md-list";
|
||||
import "./ha-md-list-item";
|
||||
import type { HaMdListItem } from "./ha-md-list-item";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
@ -407,6 +408,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-sortable .disabled=${!this.editMode} draggable-selector=".draggable" @item-moved=${this._panelMoved}>
|
||||
<ha-md-list
|
||||
class="ha-scrollbar"
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@ -420,11 +422,16 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
</ha-md-list>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
sortable = false
|
||||
) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
@ -437,17 +444,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
selectedPanel
|
||||
selectedPanel,
|
||||
sortable
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
|
||||
return html`
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, true)}
|
||||
${this._renderSpacer()}${this._renderHiddenPanels()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
selectedPanel: string,
|
||||
sortable = false
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
@ -455,7 +471,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
<ha-md-list-item
|
||||
.href=${this.editMode ? undefined : `/${urlPath}`}
|
||||
type="link"
|
||||
class=${selectedPanel === urlPath ? "selected" : ""}
|
||||
class=${classMap({
|
||||
selected: selectedPanel === urlPath,
|
||||
draggable: this.editMode && sortable,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
@ -496,15 +515,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._panelOrder = panelOrder;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
|
||||
return html`
|
||||
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
|
||||
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
|
||||
</ha-sortable>
|
||||
${this._renderSpacer()}${this._renderHiddenPanels()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHiddenPanels() {
|
||||
return html`${this._hiddenPanels.length
|
||||
? html`${this._hiddenPanels.map((url) => {
|
||||
|
@ -316,6 +316,12 @@ class StepFlowCreateEntry extends LitElement {
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.devices {
|
||||
/* header - margin content - footer */
|
||||
max-height: calc(100vh - 52px - 20px - 52px);
|
||||
}
|
||||
}
|
||||
.device {
|
||||
border: 1px solid var(--divider-color);
|
||||
padding: 6px;
|
||||
@ -352,11 +358,6 @@ class StepFlowCreateEntry extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.device {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ const DEFAULT_AGENTS = [];
|
||||
class HaBackupConfigAgents extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus!: CloudStatus;
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ attribute: false }) public agents: BackupAgent[] = [];
|
||||
|
||||
@ -48,7 +48,10 @@ class HaBackupConfigAgents extends LitElement {
|
||||
|
||||
private _description(agentId: string) {
|
||||
if (agentId === CLOUD_AGENT) {
|
||||
if (this.cloudStatus.logged_in && !this.cloudStatus.active_subscription) {
|
||||
if (
|
||||
this.cloudStatus?.logged_in &&
|
||||
!this.cloudStatus.active_subscription
|
||||
) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.backup.agents.cloud_agent_no_subcription"
|
||||
);
|
||||
@ -106,17 +109,17 @@ class HaBackupConfigAgents extends LitElement {
|
||||
}
|
||||
|
||||
private _availableAgents = memoizeOne(
|
||||
(agents: BackupAgent[], cloudStatus: CloudStatus) =>
|
||||
(agents: BackupAgent[], cloudStatus?: CloudStatus) =>
|
||||
agents.filter(
|
||||
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in
|
||||
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus?.logged_in
|
||||
)
|
||||
);
|
||||
|
||||
private _unavailableAgents = memoizeOne(
|
||||
(
|
||||
agents: BackupAgent[],
|
||||
cloudStatus: CloudStatus,
|
||||
selectedAgentIds: string[]
|
||||
selectedAgentIds: string[],
|
||||
cloudStatus?: CloudStatus
|
||||
) => {
|
||||
const availableAgentIds = this._availableAgents(agents, cloudStatus).map(
|
||||
(agent) => agent.agent_id
|
||||
@ -167,8 +170,8 @@ class HaBackupConfigAgents extends LitElement {
|
||||
);
|
||||
const unavailableAgents = this._unavailableAgents(
|
||||
this.agents,
|
||||
this.cloudStatus,
|
||||
this._value
|
||||
this._value,
|
||||
this.cloudStatus
|
||||
);
|
||||
|
||||
const allAgents = [...availableAgents, ...unavailableAgents];
|
||||
@ -187,7 +190,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
const description = this._description(agentId);
|
||||
const noCloudSubscription =
|
||||
agentId === CLOUD_AGENT &&
|
||||
this.cloudStatus.logged_in &&
|
||||
this.cloudStatus?.logged_in &&
|
||||
!this.cloudStatus.active_subscription;
|
||||
|
||||
return html`
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { clamp } from "../../../../../common/number/clamp";
|
||||
import "../../../../../components/ha-expansion-panel";
|
||||
@ -8,6 +8,7 @@ import "../../../../../components/ha-md-select";
|
||||
import type { HaMdSelect } from "../../../../../components/ha-md-select";
|
||||
import "../../../../../components/ha-md-select-option";
|
||||
import "../../../../../components/ha-md-textfield";
|
||||
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
|
||||
import type { BackupConfig, Retention } from "../../../../../data/backup";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@ -54,16 +55,21 @@ class HaBackupConfigRetention extends LitElement {
|
||||
|
||||
@state() private _value = 3;
|
||||
|
||||
@query("#value") private _customValueField?: HaMdTextfield;
|
||||
|
||||
@query("#type") private _customTypeField?: HaMdSelect;
|
||||
|
||||
private _configLoaded = false;
|
||||
|
||||
private presetOptions = [
|
||||
RetentionPreset.COPIES_3,
|
||||
RetentionPreset.FOREVER,
|
||||
RetentionPreset.CUSTOM,
|
||||
];
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
public willUpdate() {
|
||||
if (!this._configLoaded && this.retention !== undefined) {
|
||||
this._configLoaded = true;
|
||||
if (!this.retention) {
|
||||
this._preset = RetentionPreset.GLOBAL;
|
||||
} else if (
|
||||
@ -94,6 +100,10 @@ class HaBackupConfigRetention extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._configLoaded) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
@ -206,10 +216,12 @@ class HaBackupConfigRetention extends LitElement {
|
||||
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
|
||||
target.value = clamped.toString();
|
||||
|
||||
const type = this._customTypeField?.value;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
copies: this._type === "copies" ? clamped : null,
|
||||
days: this._type === "days" ? clamped : null,
|
||||
copies: type === "copies" ? clamped : null,
|
||||
days: type === "days" ? clamped : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -219,10 +231,12 @@ class HaBackupConfigRetention extends LitElement {
|
||||
const target = ev.currentTarget as HaMdSelect;
|
||||
const type = target.value as "copies" | "days";
|
||||
|
||||
const value = this._customValueField?.value;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
copies: type === "copies" ? this._value : null,
|
||||
days: type === "days" ? this._value : null,
|
||||
copies: type === "copies" ? Number(value) : null,
|
||||
days: type === "days" ? Number(value) : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -125,8 +125,10 @@ class HaConfigBackupDetails extends LitElement {
|
||||
{ location: agentName }
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.retention=${this.config?.agents[this.agentId]
|
||||
?.retention}
|
||||
.retention=${!this.config
|
||||
? undefined
|
||||
: this.config.agents[this.agentId]?.retention ||
|
||||
null}
|
||||
@value-changed=${this._retentionChanged}
|
||||
></ha-backup-config-retention>`}
|
||||
</ha-card>
|
||||
|
@ -41,7 +41,7 @@ import { brandsUrl } from "../../../util/brands-url";
|
||||
class HaConfigBackupSettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public cloudStatus!: CloudStatus;
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@ -244,7 +244,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${!this.cloudStatus.logged_in
|
||||
${!this.cloudStatus?.logged_in
|
||||
? html`<ha-card class="cloud-info">
|
||||
<div class="cloud-header">
|
||||
<img
|
||||
@ -316,7 +316,7 @@ class HaConfigBackupSettings extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
${supervisor
|
||||
? html` <ha-card>
|
||||
? html`<ha-card>
|
||||
<div class="card-header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.settings.addon_update_backup.title"
|
||||
|
@ -29,9 +29,8 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
public showDataAsHex(bytestring: string): string {
|
||||
return Array.from(new TextEncoder().encode(bytestring))
|
||||
.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0"))
|
||||
.join(" ");
|
||||
const bytes = bytestring.match(/.{2}/g) ?? [];
|
||||
return bytes.map((byte) => `0x${byte.toUpperCase()}`).join(" ");
|
||||
}
|
||||
|
||||
private async _copyToClipboard(): Promise<void> {
|
||||
|
@ -9,6 +9,7 @@ import type { SSDPDiscoveryInfoDialogParams } from "./show-dialog-ssdp-discovery
|
||||
import "../../../../../components/ha-button";
|
||||
import { showToast } from "../../../../../util/toast";
|
||||
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
|
||||
import { showSSDPRawDataDialog } from "./show-dialog-ssdp-raw-data";
|
||||
|
||||
@customElement("dialog-ssdp-device-info")
|
||||
class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
|
||||
@ -39,6 +40,16 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
|
||||
});
|
||||
}
|
||||
|
||||
private _showRawData(key: string, data: Record<string, unknown>) {
|
||||
return (e: Event) => {
|
||||
e.preventDefault();
|
||||
showSSDPRawDataDialog(this, {
|
||||
key,
|
||||
data,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@ -83,7 +94,20 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
|
||||
([key, value]) => html`
|
||||
<tr>
|
||||
<td><b>${key}</b></td>
|
||||
<td>${value}</td>
|
||||
<td>
|
||||
${typeof value === "object" && value !== null
|
||||
? html`<a
|
||||
href="#"
|
||||
@click=${this._showRawData(
|
||||
key,
|
||||
value as Record<string, unknown>
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.ssdp.show_raw_data"
|
||||
)}</a
|
||||
>`
|
||||
: value}
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
|
@ -0,0 +1,68 @@
|
||||
import { LitElement, html, nothing, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { dump } from "js-yaml";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-code-editor";
|
||||
|
||||
export interface SSDPRawDataDialogParams {
|
||||
key: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@customElement("dialog-ssdp-raw-data")
|
||||
class DialogSSDPRawData extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: SSDPRawDataDialogParams;
|
||||
|
||||
public async showDialog(params: SSDPRawDataDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
`${this.hass.localize("ui.panel.config.ssdp.raw_data_title")}: ${this._params.key}`
|
||||
)}
|
||||
>
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
.value=${dump(this._params.data)}
|
||||
readonly
|
||||
autofocus
|
||||
></ha-code-editor>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-code-editor {
|
||||
--code-mirror-max-height: 60vh;
|
||||
--code-mirror-height: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-ssdp-raw-data": DialogSSDPRawData;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
|
||||
export interface SSDPRawDataDialogParams {
|
||||
key: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const loadSSDPRawDataDialog = () => import("./dialog-ssdp-raw-data");
|
||||
|
||||
export const showSSDPRawDataDialog = (
|
||||
element: HTMLElement,
|
||||
ssdpRawDataDialogParams: SSDPRawDataDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-ssdp-raw-data",
|
||||
dialogImport: loadSSDPRawDataDialog,
|
||||
dialogParams: ssdpRawDataDialogParams,
|
||||
});
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const sidebarEditStyle = css`
|
||||
ha-sortable ha-md-list-item:nth-child(2n) {
|
||||
ha-sortable ha-md-list-item.draggable:nth-child(2n) {
|
||||
animation-name: keyframes1;
|
||||
animation-iteration-count: infinite;
|
||||
transform-origin: 50% 10%;
|
||||
@ -9,7 +9,7 @@ export const sidebarEditStyle = css`
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
ha-sortable ha-md-list-item:nth-child(2n-1) {
|
||||
ha-sortable ha-md-list-item.draggable:nth-child(2n-1) {
|
||||
animation-name: keyframes2;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
@ -18,8 +18,7 @@ export const sidebarEditStyle = css`
|
||||
animation-duration: 0.33s;
|
||||
}
|
||||
|
||||
ha-sortable ha-md-list-item {
|
||||
height: 48px;
|
||||
ha-sortable ha-md-list-item.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
|
@ -5586,7 +5586,9 @@
|
||||
"upnp": "Universal Plug and Play (UPnP)",
|
||||
"discovery_information": "Discovery information",
|
||||
"copy_to_clipboard": "Copy to clipboard",
|
||||
"no_devices_found": "No matching SSDP/UPnP discoveries found"
|
||||
"no_devices_found": "No matching SSDP/UPnP discoveries found",
|
||||
"show_raw_data": "Show raw data",
|
||||
"raw_data_title": "Raw data"
|
||||
},
|
||||
"zeroconf": {
|
||||
"name": "Name",
|
||||
|
Loading…
x
Reference in New Issue
Block a user