Merge branch 'dev' into rc

This commit is contained in:
Bram Kragten 2024-12-24 16:23:46 +01:00
commit f416b1b5da
45 changed files with 374 additions and 248 deletions

View File

@ -27,7 +27,7 @@
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.0",
"@braintree/sanitize-url": "7.1.0",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.4",
"@codemirror/commands": "6.7.1",
"@codemirror/language": "6.10.7",
@ -139,7 +139,7 @@
"tinykeys": "3.0.0",
"tsparticles-engine": "2.12.0",
"tsparticles-preset-links": "2.12.0",
"ua-parser-js": "1.0.39",
"ua-parser-js": "1.0.40",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"vue": "2.7.16",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20241223.1"
version = "20241224.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -61,6 +61,8 @@ export class HaChartBase extends LitElement {
@state() private _chartHeight?: number;
@state() private _legendHeight?: number;
@state() private _tooltip?: Tooltip;
@state() private _hiddenDatasets: Set<number> = new Set();
@ -214,10 +216,22 @@ export class HaChartBase extends LitElement {
this.chart.update("none");
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("data") || changedProperties.has("options")) {
if (this.options?.plugins?.legend?.display) {
this._legendHeight =
this.renderRoot.querySelector(".chart-legend")?.clientHeight;
} else {
this._legendHeight = 0;
}
}
}
protected render() {
return html`
${this.options?.plugins?.legend?.display === true
? html`<div class="chartLegend">
? html`<div class="chart-legend">
<ul>
${this._datasetOrder.map((index) => {
const dataset = this.data.datasets[index];
@ -249,7 +263,7 @@ export class HaChartBase extends LitElement {
</div>`
: ""}
<div
class="animationContainer"
class="animation-container"
style=${styleMap({
height: `${this.height || this._chartHeight || 0}px`,
overflow: this._chartHeight ? "initial" : "hidden",
@ -288,7 +302,7 @@ export class HaChartBase extends LitElement {
</div>
${this._tooltip
? html`<div
class="chartTooltip ${classMap({
class="chart-tooltip ${classMap({
[this._tooltip.yAlign]: true,
})}"
style=${styleMap({
@ -298,7 +312,7 @@ export class HaChartBase extends LitElement {
>
<div class="title">${this._tooltip.title}</div>
${this._tooltip.beforeBody
? html`<div class="beforeBody">
? html`<div class="before-body">
${this._tooltip.beforeBody}
</div>`
: ""}
@ -456,6 +470,7 @@ export class HaChartBase extends LitElement {
private _handleChartScroll(ev: MouseEvent) {
const modifier = isMac ? "metaKey" : "ctrlKey";
this._tooltip = undefined;
if (!ev[modifier] && !this._showZoomHint) {
this._showZoomHint = true;
setTimeout(() => {
@ -498,15 +513,20 @@ export class HaChartBase extends LitElement {
this._tooltip = undefined;
return;
}
const boundingBox = this.getBoundingClientRect();
this._tooltip = {
...context.tooltip,
top: this.chart!.canvas.offsetTop + context.tooltip.caretY + 12 + "px",
top:
boundingBox.y +
(this._legendHeight || 0) +
context.tooltip.caretY +
12 +
"px",
left:
this.chart!.canvas.offsetLeft +
clamp(
context.tooltip.caretX,
100,
this.clientWidth - 100 - this._paddingYAxisInternal
boundingBox.x + context.tooltip.caretX,
boundingBox.x + 100,
boundingBox.x + boundingBox.width - 100
) -
100 +
"px",
@ -525,16 +545,13 @@ export class HaChartBase extends LitElement {
return css`
:host {
display: block;
position: var(--chart-base-position, relative);
position: relative;
}
.animationContainer {
.animation-container {
overflow: hidden;
height: 0;
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
.chart-container {
position: relative;
}
canvas {
max-height: var(--chart-max-height, 400px);
}
@ -542,10 +559,10 @@ export class HaChartBase extends LitElement {
/* allow scrolling if the chart is not zoomed */
touch-action: pan-y !important;
}
.chartLegend {
.chart-legend {
text-align: center;
}
.chartLegend li {
.chart-legend li {
cursor: pointer;
display: inline-grid;
grid-auto-flow: column;
@ -554,16 +571,16 @@ export class HaChartBase extends LitElement {
align-items: center;
color: var(--secondary-text-color);
}
.chartLegend .hidden {
.chart-legend .hidden {
text-decoration: line-through;
}
.chartLegend .label {
.chart-legend .label {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.chartLegend .bullet,
.chartTooltip .bullet {
.chart-legend .bullet,
.chart-tooltip .bullet {
border-width: 1px;
border-style: solid;
border-radius: 50%;
@ -577,13 +594,13 @@ export class HaChartBase extends LitElement {
margin-inline-start: initial;
direction: var(--direction);
}
.chartTooltip .bullet {
.chart-tooltip .bullet {
align-self: baseline;
}
.chartTooltip {
.chart-tooltip {
padding: 8px;
font-size: 90%;
position: absolute;
position: fixed;
background: rgba(80, 80, 80, 0.9);
color: white;
border-radius: 4px;
@ -596,17 +613,17 @@ export class HaChartBase extends LitElement {
box-sizing: border-box;
direction: var(--direction);
}
.chartLegend ul,
.chartTooltip ul {
.chart-legend ul,
.chart-tooltip ul {
display: inline-block;
padding: 0 0px;
margin: 8px 0 0 0;
width: 100%;
}
.chartTooltip ul {
.chart-tooltip ul {
margin: 0 4px;
}
.chartTooltip li {
.chart-tooltip li {
display: flex;
white-space: pre-line;
word-break: break-word;
@ -614,16 +631,16 @@ export class HaChartBase extends LitElement {
line-height: 16px;
padding: 4px 0;
}
.chartTooltip .title {
.chart-tooltip .title {
text-align: center;
font-weight: 500;
word-break: break-word;
direction: ltr;
}
.chartTooltip .footer {
.chart-tooltip .footer {
font-weight: 500;
}
.chartTooltip .beforeBody {
.chart-tooltip .before-body {
text-align: center;
font-weight: 300;
word-break: break-all;

View File

@ -210,11 +210,9 @@ class DialogMediaManage extends LitElement {
href="/config/storage"
@click=${this.closeDialog}
>
${this.hass
.localize(
"ui.components.media-browser.file_management.tip_storage_panel"
)
.toLowerCase()}
${this.hass.localize(
"ui.components.media-browser.file_management.tip_storage_panel"
)}
</a>`,
}
)}

View File

@ -1,6 +1,16 @@
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { domainToName } from "./integration";
import type { FrontendLocaleData } from "./translation";
import {
formatDateTime,
formatDateTimeNumeric,
} from "../common/datetime/format_date_time";
import { fileDownload } from "../util/file_download";
export const enum BackupScheduleState {
NEVER = "never",
@ -282,3 +292,49 @@ export const generateEncryptionKey = () => {
});
return result;
};
export const generateEmergencyKit = (
hass: HomeAssistant,
encryptionKey: string
) =>
"data:text/plain;charset=utf-8," +
encodeURIComponent(`Home Assistant Backup Emergency Kit
This emergency kit contains your backup encryption key. You need this key
to be able to restore your Home Assistant backups.
Date: ${formatDateTime(new Date(), hass.locale, hass.config)}
Instance:
${hass.config.location_name}
URL:
${hass.auth.data.hassUrl}
Encryption key:
${encryptionKey}
For more information visit: https://www.home-assistant.io/more-info/backup-emergency-kit`);
export const geneateEmergencyKitFileName = (
hass: HomeAssistant,
append?: string
) =>
`home_assistant_backup_emergency_kit_${append ? `${append}_` : ""}${formatDateTimeNumeric(new Date(), hass.locale, hass.config).replace(",", "").replace(" ", "_")}.txt`;
export const downloadEmergencyKit = (
hass: HomeAssistant,
key: string,
appendFileName?: string
) =>
fileDownload(
generateEmergencyKit(hass, key),
geneateEmergencyKitFileName(hass, appendFileName)
);
export const getFormattedBackupTime = memoizeOne(
(locale: FrontendLocaleData, config: HassConfig) => {
const date = setMinutes(setHours(new Date(), 4), 45);
return formatTime(date, locale, config);
}
);

View File

@ -50,7 +50,7 @@ export const showConfigFlowDialog = (
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: step.reason;
},
@ -71,7 +71,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},
@ -163,7 +163,7 @@ export const showConfigFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -184,7 +184,7 @@ export const showConfigFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -214,7 +214,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},
@ -234,7 +234,7 @@ export const showConfigFlowDialog = (
);
return description
? html`
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
<ha-markdown allow-svg breaks .content=${description}></ha-markdown>
`
: "";
},

View File

@ -61,7 +61,7 @@ export const showOptionsFlowDialog = (
? html`
<ha-markdown
breaks
allowsvg
allow-svg
.content=${description}
></ha-markdown>
`
@ -85,7 +85,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -183,7 +183,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -207,7 +207,7 @@ export const showOptionsFlowDialog = (
return description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>

View File

@ -51,7 +51,6 @@ class StepFlowAbort extends LitElement {
}
private async _handleMissingCreds() {
this._flowDone();
// Prompt to enter credentials and restart integration setup
showAddApplicationCredentialDialog(this.params.dialogParentElement!, {
selectedDomain: this.domain,
@ -64,6 +63,7 @@ class StepFlowAbort extends LitElement {
});
},
});
this._flowDone();
}
private _flowDone(): void {

View File

@ -213,9 +213,10 @@ class MoreInfoMediaPlayer extends LitElement {
ha-icon-button[action="turn_off"],
ha-icon-button[action="turn_on"] {
margin-inline-end: auto;
margin-right: auto;
margin-left: inherit;
margin-inline-start: inherit;
margin-inline-end: auto;
}
.controls {

View File

@ -545,11 +545,7 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
/* This is needed for the tooltip of the history charts to be positioned correctly */
--dialog-surface-position: static;
--dialog-content-position: static;
--dialog-content-padding: 0;
--chart-base-position: static;
}
.content {

View File

@ -40,6 +40,7 @@ import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
import { computeDeviceName } from "../../data/device_registry";
interface QuickBarItem extends ScorableTextItem {
primaryText: string;
@ -522,12 +523,14 @@ export class QuickBar extends LitElement {
}
private _generateDeviceItems(): DeviceItem[] {
return Object.keys(this.hass.devices)
.map((deviceId) => {
const device = this.hass.devices[deviceId];
const area = this.hass.areas[device.area_id!];
return Object.values(this.hass.devices)
.filter((device) => !device.disabled_by)
.map((device) => {
const area = device.area_id
? this.hass.areas[device.area_id]
: undefined;
const deviceItem = {
primaryText: device.name!,
primaryText: computeDeviceName(device, this.hass),
deviceId: device.id,
area: area?.name,
action: () => navigate(`/config/devices/device/${device.id}`),

View File

@ -14,6 +14,7 @@ import "../../category/ha-category-picker";
import "../../../../components/ha-expansion-panel";
import "../../../../components/chips/ha-chip-set";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/ha-area-picker";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
@ -57,6 +58,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
);
this._newDescription = params.config.description || "";
this._entryUpdates = params.entityRegistryUpdate || {
area: params.entityRegistryEntry?.area_id || "",
labels: params.entityRegistryEntry?.labels || [],
category: params.entityRegistryEntry?.categories[params.domain] || "",
};
@ -66,6 +68,7 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._newIcon ? "icon" : "",
this._entryUpdates.category ? "category" : "",
this._entryUpdates.labels.length > 0 ? "labels" : "",
this._entryUpdates.area ? "area" : "",
];
}
@ -193,6 +196,14 @@ class DialogAutomationRename extends LitElement implements HassDialog {
@value-changed=${this._registryEntryChanged}
></ha-labels-picker>`
: nothing}
${this._visibleOptionals.includes("area")
? html` <ha-area-picker
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
: nothing}
<ha-chip-set>
${this._renderOptionalChip(
@ -209,6 +220,12 @@ class DialogAutomationRename extends LitElement implements HassDialog {
)
)
: nothing}
${this._renderOptionalChip(
"area",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_area"
)
)}
${this._renderOptionalChip(
"category",
this.hass.localize(
@ -311,12 +328,14 @@ class DialogAutomationRename extends LitElement implements HassDialog {
ha-icon-picker,
ha-category-picker,
ha-labels-picker,
ha-area-picker,
ha-chip-set {
display: block;
}
ha-icon-picker,
ha-category-picker,
ha-labels-picker,
ha-area-picker,
ha-chip-set {
margin-top: 16px;
}

View File

@ -13,6 +13,7 @@ interface BaseRenameDialogParams {
}
export interface EntityRegistryUpdate {
area: string;
labels: string[];
category: string;
}

View File

@ -167,10 +167,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
if (
this._entityRegCreated &&
this._newAutomationId &&
changedProps.has("entityRegistry")
changedProps.has("_entityRegistry")
) {
const automation = this._entityRegistry.find(
(entity: EntityRegistryEntry) =>
entity.platform === "automation" &&
entity.unique_id === this._newAutomationId
);
if (automation) {
@ -927,6 +928,14 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._saving = true;
this._validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
}
try {
await saveAutomationConfig(this.hass, id, this._config!);
@ -934,13 +943,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
let entityId = this._entityId;
// wait for automation to appear in entity registry when creating a new automation
if (!entityId) {
this._newAutomationId = id;
const automation = await new Promise<EntityRegistryEntry>(
(resolve) => {
this._entityRegCreated = resolve;
}
);
if (entityRegPromise) {
const automation = await entityRegPromise;
entityId = automation.entity_id;
}
@ -950,6 +954,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
automation: this._entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
});
}
}

View File

@ -51,7 +51,7 @@ class HaBackupConfigAgents extends LitElement {
private _description(agentId: string) {
if (agentId === CLOUD_AGENT) {
return "Note: It stores only one backup, regardless of your settings.";
return "Note: It stores only one backup with a maximum size of 5 GB, regardless of your settings.";
}
if (isNetworkMountAgent(agentId)) {
return "Network storage";

View File

@ -6,9 +6,10 @@ import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import type { HomeAssistant } from "../../../../../types";
import { showChangeBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-change-backup-encryption-key";
import { fileDownload } from "../../../../../util/file_download";
import { showSetBackupEncryptionKeyDialog } from "../../dialogs/show-dialog-set-backup-encryption-key";
import { downloadEmergencyKit } from "../../../../../data/backup";
@customElement("ha-backup-config-encryption-key")
class HaBackupConfigEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -64,10 +65,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
if (!this._value) {
return;
}
fileDownload(
"data:text/plain;charset=utf-8," + encodeURIComponent(this._value),
"emergency_kit.txt"
);
downloadEmergencyKit(this.hass, this._value);
}
private _change() {

View File

@ -3,18 +3,21 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-textfield";
import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-switch";
import type { BackupConfig } from "../../../../../data/backup";
import { BackupScheduleState } from "../../../../../data/backup";
import {
BackupScheduleState,
getFormattedBackupTime,
} from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types";
import { clamp } from "../../../../../common/number/clamp";
export type BackupConfigSchedule = Pick<BackupConfig, "schedule" | "retention">;
@ -120,13 +123,12 @@ class HaBackupConfigSchedule extends LitElement {
protected render() {
const data = this._getData(this.value);
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
return html`
<ha-md-list>
<ha-md-list-item>
<span slot="headline">Use automatic backups</span>
<span slot="supporting-text">
How often you want to create a backup.
</span>
<ha-switch
slot="end"
@ -148,35 +150,36 @@ class HaBackupConfigSchedule extends LitElement {
.value=${data.schedule}
>
<ha-md-select-option .value=${BackupScheduleState.DAILY}>
<div slot="headline">Daily at 04:45</div>
<div slot="headline">Daily at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.MONDAY}>
<div slot="headline">Monday at 04:45</div>
<div slot="headline">Monday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.TUESDAY}>
<div slot="headline">Tuesday at 04:45</div>
<div slot="headline">Tuesday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.WEDNESDAY}>
<div slot="headline">Wednesday at 04:45</div>
<div slot="headline">Wednesday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.THURSDAY}>
<div slot="headline">Thursday at 04:45</div>
<div slot="headline">Thursday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.FRIDAY}>
<div slot="headline">Friday at 04:45</div>
<div slot="headline">Friday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.SATURDAY}>
<div slot="headline">Saturday at 04:45</div>
<div slot="headline">Saturday at ${time}</div>
</ha-md-select-option>
<ha-md-select-option .value=${BackupScheduleState.SUNDAY}>
<div slot="headline">Sunday at 04:45</div>
<div slot="headline">Sunday at ${time}</div>
</ha-md-select-option>
</ha-md-select>
</ha-md-list-item>
<ha-md-list-item>
<span slot="headline">Backups to keep</span>
<span slot="supporting-text">
The number of backups that are saved
Based on the maximum number of backups or how many days they
should be kept.
</span>
<ha-md-select
slot="end"
@ -326,16 +329,13 @@ class HaBackupConfigSchedule extends LitElement {
@media all and (max-width: 450px) {
ha-md-select {
min-width: 160px;
width: 160px;
}
}
ha-md-textfield#value {
min-width: 70px;
width: 70px;
}
ha-md-select#type {
min-width: 100px;
width: 100px;
}
`;
}

View File

@ -2,7 +2,9 @@ import { mdiPuzzle } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield";
@ -29,10 +31,16 @@ export class HaBackupAddonsPicker extends LitElement {
@property({ attribute: "hide-version", type: Boolean })
public hideVersion = false;
private _addons = memoizeOne((addons: BackupAddonItem[]) =>
addons.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
)
);
protected render() {
return html`
<div class="items">
${this.addons.map(
${this._addons(this.addons).map(
(item) => html`
<ha-formfield>
<ha-backup-formfield-label

View File

@ -300,22 +300,22 @@ export class HaBackupDataPicker extends LitElement {
static get styles(): CSSResultGroup {
return css`
.section {
margin-inline-start: -16px;
margin-inline-end: 0;
margin-left: -16px;
margin-inline-start: -16px;
margin-inline-end: initial;
}
.items {
padding-inline-start: 40px;
padding-inline-end: 0;
padding-left: 40px;
padding-inline-start: 40px;
padding-inline-end: initial;
display: flex;
flex-direction: column;
}
ha-backup-addons-picker {
display: block;
padding-inline-start: 40px;
padding-inline-end: 0;
padding-left: 40px;
padding-inline-start: 40px;
padding-inline-end: initial;
}
`;
}

View File

@ -68,6 +68,9 @@ class HaBackupSummaryCard extends LitElement {
}
static styles = css`
ha-card {
min-height: 74px;
}
.summary {
display: flex;
flex-direction: row;

View File

@ -13,6 +13,7 @@ import type { BackupConfig } from "../../../../../data/backup";
import {
BackupScheduleState,
computeBackupAgentName,
getFormattedBackupTime,
isLocalAgent,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
@ -43,30 +44,32 @@ class HaBackupBackupsSummary extends LitElement {
copiesText = `and keep backups for ${days} day(s)`;
}
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
let scheduleText = "";
if (schedule === BackupScheduleState.DAILY) {
scheduleText = `Daily at 04:45`;
scheduleText = `Daily at ${time}`;
}
if (schedule === BackupScheduleState.MONDAY) {
scheduleText = `Weekly on Mondays at 04:45`;
scheduleText = `Weekly on Mondays at ${time}`;
}
if (schedule === BackupScheduleState.TUESDAY) {
scheduleText = `Weekly on Thuesdays at 04:45`;
scheduleText = `Weekly on Tuesdays at ${time}`;
}
if (schedule === BackupScheduleState.WEDNESDAY) {
scheduleText = `Weekly on Wednesdays at 04:45`;
scheduleText = `Weekly on Wednesdays at ${time}`;
}
if (schedule === BackupScheduleState.THURSDAY) {
scheduleText = `Weekly on Thursdays at 04:45`;
scheduleText = `Weekly on Thursdays at ${time}`;
}
if (schedule === BackupScheduleState.FRIDAY) {
scheduleText = `Weekly on Fridays at 04:45`;
scheduleText = `Weekly on Fridays at ${time}`;
}
if (schedule === BackupScheduleState.SATURDAY) {
scheduleText = `Weekly on Saturdays at 04:45`;
scheduleText = `Weekly on Saturdays at ${time}`;
}
if (schedule === BackupScheduleState.SUNDAY) {
scheduleText = `Weekly on Sundays at 04:45`;
scheduleText = `Weekly on Sundays at ${time}`;
}
return scheduleText + " " + copiesText;

View File

@ -1,10 +1,9 @@
import { mdiBackupRestore, mdiCalendar } from "@mdi/js";
import { addHours, differenceInDays, setHours, setMinutes } from "date-fns";
import { addHours, differenceInDays } from "date-fns";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { formatTime } from "../../../../../common/datetime/format_time";
import { relativeTime } from "../../../../../common/datetime/relative_time";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
@ -12,7 +11,10 @@ import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { BackupConfig, BackupContent } from "../../../../../data/backup";
import { BackupScheduleState } from "../../../../../data/backup";
import {
BackupScheduleState,
getFormattedBackupTime,
} from "../../../../../data/backup";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../ha-backup-summary-card";
@ -29,20 +31,16 @@ class HaBackupOverviewBackups extends LitElement {
@property({ type: Boolean }) public fetching = false;
private _lastBackup = memoizeOne((backups: BackupContent[]) => {
private _lastSuccessfulBackup = memoizeOne((backups: BackupContent[]) => {
const sortedBackups = backups
.filter(
(backup) =>
backup.with_automatic_settings && !backup.failed_agent_ids?.length
)
.filter((backup) => backup.with_automatic_settings)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return sortedBackups[0] as BackupContent | undefined;
});
private _nextBackupDescription(schedule: BackupScheduleState) {
const newDate = setMinutes(setHours(new Date(), 4), 45);
const time = formatTime(newDate, this.hass.locale, this.hass.config);
const time = getFormattedBackupTime(this.hass.locale, this.hass.config);
switch (schedule) {
case BackupScheduleState.DAILY:
@ -84,37 +82,27 @@ class HaBackupOverviewBackups extends LitElement {
`;
}
const lastBackup = this._lastBackup(this.backups);
const lastSuccessfulBackup = this._lastSuccessfulBackup(this.backups);
if (!lastBackup) {
return html`
<ha-backup-summary-card
heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning"
>
</ha-backup-summary-card>
`;
}
const lastBackupDate = new Date(lastBackup.date);
const now = new Date();
const lastBackupDescription = `Last successful backup ${relativeTime(lastBackupDate, this.hass.locale, now, true)} and stored to ${lastBackup.agent_ids?.length} locations.`;
const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state
);
const lastSuccessfulBackupDate = lastSuccessfulBackup
? new Date(lastSuccessfulBackup.date)
: new Date(0);
const lastAttempt = this.config.last_attempted_automatic_backup
? new Date(this.config.last_attempted_automatic_backup)
: undefined;
if (lastAttempt && lastAttempt > lastBackupDate) {
const now = new Date();
const lastBackupDescription = lastSuccessfulBackup
? `Last successful backup ${relativeTime(lastSuccessfulBackupDate, this.hass.locale, now, true)} and stored to ${lastSuccessfulBackup.agent_ids?.length} locations.`
: "You have no successful backups.";
if (lastAttempt && lastAttempt > lastSuccessfulBackupDate) {
const lastAttemptDescription = `The last automatic backup trigged ${relativeTime(lastAttempt, this.hass.locale, now, true)} wasn't successful.`;
return html`
<ha-backup-summary-card
heading=${`Last automatic backup failed`}
heading="Last automatic backup failed"
status="error"
>
<ha-md-list>
@ -131,10 +119,25 @@ class HaBackupOverviewBackups extends LitElement {
`;
}
if (!lastSuccessfulBackup) {
return html`
<ha-backup-summary-card
heading="No automatic backup available"
description="You have no automatic backups yet."
status="warning"
>
</ha-backup-summary-card>
`;
}
const nextBackupDescription = this._nextBackupDescription(
this.config.schedule.state
);
const numberOfDays = differenceInDays(
// Subtract a few hours to avoid showing as overdue if it's just a few hours (e.g. daylight saving)
addHours(now, -OVERDUE_MARGIN_HOURS),
lastBackupDate
lastSuccessfulBackupDate
);
const isOverdue =
@ -216,12 +219,13 @@ class HaBackupOverviewBackups extends LitElement {
animation-timing-function: linear;
animation-duration: 1.2s;
border-radius: 4px;
height: 20px;
height: 16px;
margin: 2px 0;
background: linear-gradient(
to right,
rgb(247, 249, 250) 8%,
rgb(235, 238, 240) 18%,
rgb(247, 249, 250) 33%
var(--card-background-color) 8%,
var(--secondary-background-color) 18%,
var(--card-background-color) 33%
)
0% 0% / 936px 104px;
}

View File

@ -24,6 +24,7 @@ import {
BackupScheduleState,
CLOUD_AGENT,
CORE_LOCAL_AGENT,
downloadEmergencyKit,
generateEncryptionKey,
HASSIO_LOCAL_AGENT,
updateBackupConfig,
@ -31,7 +32,6 @@ import {
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import { showToast } from "../../../../util/toast";
import "../components/config/ha-backup-config-agents";
import "../components/config/ha-backup-config-data";
@ -101,7 +101,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
agents.push(CORE_LOCAL_AGENT);
}
// Enable cloud location if logged in
if (this._params.cloudStatus.logged_in) {
if (this._params.cloudStatus?.logged_in) {
agents.push(CLOUD_AGENT);
}
@ -327,12 +327,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
`;
case "setup":
return html`
<p>
It is recommended that you create a backup every day. You should
keep three backups in at least two different locations, one of which
should be off-site. Once you make your selection, your first backup
will begin.
</p>
<ha-md-list class="full">
<ha-md-list-item type="button" @click=${this._done}>
<span slot="headline">Recommended settings</span>
@ -398,14 +392,11 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
if (!key) {
return;
}
fileDownload(
"data:text/plain;charset=utf-8," + encodeURIComponent(key),
"emergency_kit.txt"
);
downloadEmergencyKit(this.hass, key);
}
private _copyKeyToClipboard() {
copyToClipboard(this._config!.create_backup.password!);
private async _copyKeyToClipboard() {
await copyToClipboard(this._config!.create_backup.password!);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@ -471,6 +462,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
max-height: min(605px, 100% - 48px);
}
ha-md-list {
background: none;

View File

@ -13,11 +13,13 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import { generateEncryptionKey } from "../../../../data/backup";
import {
downloadEmergencyKit,
generateEncryptionKey,
} from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import { showToast } from "../../../../util/toast";
import type { ChangeBackupEncryptionKeyDialogParams } from "./show-dialog-change-backup-encryption-key";
@ -203,18 +205,18 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
return nothing;
}
private _copyKeyToClipboard() {
copyToClipboard(this._newEncryptionKey);
private async _copyKeyToClipboard() {
await copyToClipboard(this._newEncryptionKey);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
private _copyOldKeyToClipboard() {
private async _copyOldKeyToClipboard() {
if (!this._params?.currentKey) {
return;
}
copyToClipboard(this._params.currentKey);
await copyToClipboard(this._params.currentKey);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@ -224,22 +226,14 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._params?.currentKey) {
return;
}
fileDownload(
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._params.currentKey),
"emergency_kit_old.txt"
);
downloadEmergencyKit(this.hass, this._params.currentKey, "old");
}
private _downloadNew() {
if (!this._newEncryptionKey) {
return;
}
fileDownload(
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._newEncryptionKey),
"emergency_kit.txt"
);
downloadEmergencyKit(this.hass, this._newEncryptionKey);
}
private async _submit() {

View File

@ -99,7 +99,9 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
const { agents } = await fetchBackupAgentsInfo(this.hass);
this._agentIds = agents
.map((agent) => agent.agent_id)
.filter((id) => id !== CLOUD_AGENT || this._params?.cloudStatus.logged_in)
.filter(
(id) => id !== CLOUD_AGENT || this._params?.cloudStatus?.logged_in
)
.sort(compareAgents);
}

View File

@ -11,11 +11,13 @@ import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import { generateEncryptionKey } from "../../../../data/backup";
import {
downloadEmergencyKit,
generateEncryptionKey,
} from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { fileDownload } from "../../../../util/file_download";
import type { SetBackupEncryptionKeyDialogParams } from "./show-dialog-set-backup-encryption-key";
const STEPS = ["new", "save"] as const;
@ -162,11 +164,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._newEncryptionKey) {
return;
}
fileDownload(
"data:text/plain;charset=utf-8," +
encodeURIComponent(this._newEncryptionKey),
"emergency_kit.txt"
);
downloadEmergencyKit(this.hass, this._newEncryptionKey);
}
private _encryptionKeyChanged(ev) {

View File

@ -4,7 +4,7 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface BackupOnboardingDialogParams {
submit?: (value: boolean) => void;
cancel?: () => void;
cloudStatus: CloudStatus;
cloudStatus?: CloudStatus;
}
const loadDialog = () => import("./dialog-backup-onboarding");

View File

@ -5,7 +5,7 @@ import type { CloudStatus } from "../../../../data/cloud";
export interface GenerateBackupDialogParams {
submit?: (response: GenerateBackupParams) => void;
cancel?: () => void;
cloudStatus: CloudStatus;
cloudStatus?: CloudStatus;
}
export const loadGenerateBackupDialog = () =>

View File

@ -78,7 +78,7 @@ const TYPE_ORDER: Array<BackupType> = ["automatic", "manual", "imported"];
class HaConfigBackupBackups extends SubscribeMixin(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;
@ -167,7 +167,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
title: "Locations",
showNarrow: true,
minWidth: "60px",
maxWidth: "120px",
template: (backup) => html`
<div style="display: flex; gap: 4px;">
${(backup.agent_ids || []).map((agentId) => {
@ -181,7 +180,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
<ha-svg-icon
.path=${mdiHarddisk}
title=${name}
slot="graphic"
style="flex-shrink: 0;"
></ha-svg-icon>
`;
}
@ -190,7 +189,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
<ha-svg-icon
.path=${mdiNas}
title=${name}
slot="graphic"
style="flex-shrink: 0;"
></ha-svg-icon>
`;
}
@ -209,6 +208,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
referrerpolicy="no-referrer"
alt=${name}
slot="graphic"
style="flex-shrink: 0;"
/>
`;
})}

View File

@ -26,7 +26,6 @@ import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "./components/ha-backup-summary-card";
import "./components/overview/ha-backup-overview-backups";
import "./components/overview/ha-backup-overview-onboarding";
import "./components/overview/ha-backup-overview-progress";
@ -42,7 +41,7 @@ import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
class HaConfigBackupOverview 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;

View File

@ -141,9 +141,9 @@ class HaConfigBackupSettings extends LitElement {
<div class="card-content">
<p>
Keep this encryption key in a safe place, as you will need it to
access your backup, allowing it to be restored. Either record
the characters below or download them as an emergency kit file.
Encryption keeps your backups private and secure.
access your backup, allowing it to be restored. Download them as
an emergency kit file and store it somewhere safe. Encryption
keeps your backups private and secure.
</p>
<ha-backup-config-encryption-key
.hass=${this.hass}

View File

@ -75,7 +75,9 @@ export class DialogTryTts extends LitElement {
<ha-textarea
autogrow
id="message"
label="Message"
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.message"
)}
.value=${this._message ||
this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.example_message",

View File

@ -247,6 +247,13 @@ export class SystemLogCard extends LitElement {
padding-top: 8px;
}
:host {
direction: var(--direction);
}
mwc-list {
direction: ltr;
}
.header {
display: flex;
justify-content: space-between;
@ -293,13 +300,7 @@ export class SystemLogCard extends LitElement {
border-top: 1px solid var(--divider-color);
}
.card-actions,
.empty-content {
direction: var(--direction);
}
.row-secondary {
direction: var(--direction);
text-align: left;
}
`;

View File

@ -105,6 +105,9 @@ export class HaConfigLovelaceRescources extends LitElement {
},
delete: {
title: "",
label: localize(
"ui.panel.config.lovelace.resources.picker.headers.delete"
),
type: "icon-button",
minWidth: "48px",
maxWidth: "48px",

View File

@ -99,7 +99,7 @@ class DialogRepairsIssue extends LitElement {
: ""}
<ha-markdown
id="dialog-repairs-issue-description"
allowsvg
allow-svg
breaks
@click=${this._clickHandler}
.content=${this.hass.localize(

View File

@ -90,7 +90,7 @@ export const showRepairsFlowDialog = (
? html`
<ha-markdown
breaks
allowsvg
allow-svg
.content=${description}
></ha-markdown>
`
@ -123,7 +123,7 @@ export const showRepairsFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -220,7 +220,7 @@ export const showRepairsFlowDialog = (
return html`${renderIssueDescription(hass, issue)}${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>
@ -254,7 +254,7 @@ export const showRepairsFlowDialog = (
${description
? html`
<ha-markdown
allowsvg
allow-svg
breaks
.content=${description}
></ha-markdown>

View File

@ -139,7 +139,8 @@ export class HaScriptEditor extends SubscribeMixin(
changedProps.has("entityRegistry")
) {
const script = this.entityRegistry.find(
(entity: EntityRegistryEntry) => entity.unique_id === this._newScriptId
(entity: EntityRegistryEntry) =>
entity.platform === "script" && entity.unique_id === this._newScriptId
);
if (script) {
this._entityRegCreated(script);
@ -164,7 +165,8 @@ export class HaScriptEditor extends SubscribeMixin(
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${!this._config.alias ? "" : this._config.alias}
.header=${this._config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this.scriptId && !this.narrow
? html`
@ -487,9 +489,7 @@ export class HaScriptEditor extends SubscribeMixin(
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {
alias: this.hass.localize("ui.panel.config.script.editor.default_name"),
};
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
}
@ -894,6 +894,15 @@ export class HaScriptEditor extends SubscribeMixin(
const id = this.scriptId || this._entityId || Date.now();
this._saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
}
try {
await this.hass!.callApi(
"POST",
@ -902,23 +911,20 @@ export class HaScriptEditor extends SubscribeMixin(
);
if (this._entityRegistryUpdate !== undefined) {
let entityId = id.toString().startsWith("script.")
? id.toString()
: `script.${id}`;
let entityId = this._entityId;
// wait for new script to appear in entity registry
if (!this.scriptId) {
const script = await new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
if (entityRegPromise) {
const script = await entityRegPromise;
entityId = script.entity_id;
}
await updateEntityRegistryEntry(this.hass, entityId, {
await updateEntityRegistryEntry(this.hass, entityId!, {
categories: {
script: this._entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
});
}

View File

@ -178,7 +178,9 @@ export class HaScriptTrace extends LitElement {
<ha-icon-button
.disabled=${this._traces[this._traces.length - 1].run_id ===
this._runId}
label="Older trace"
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.older_trace"
)}
@click=${this._pickOlderTrace}
.path=${mdiRayEndArrow}
></ha-icon-button>
@ -198,7 +200,9 @@ export class HaScriptTrace extends LitElement {
</select>
<ha-icon-button
.disabled=${this._traces[0].run_id === this._runId}
label="Newer trace"
.label=${this.hass!.localize(
"ui.panel.config.automation.trace.newer_trace"
)}
@click=${this._pickNewerTrace}
.path=${mdiRayStartArrow}
></ha-icon-button>

View File

@ -324,10 +324,10 @@ class HaPanelDevState extends LitElement {
`;
}
private _copyEntity(ev) {
private async _copyEntity(ev) {
ev.preventDefault();
const entity = (ev.currentTarget! as any).entity;
copyToClipboard(entity.entity_id);
await copyToClipboard(entity.entity_id);
}
private _entitySelected(ev) {

View File

@ -23,7 +23,7 @@ export class HuiGenericEntityRow extends LitElement {
@property({ attribute: false }) public config?: EntitiesCardEntityConfig;
@property({ attribute: false }) public secondaryText?: string;
@property({ attribute: "secondary-text" }) public secondaryText?: string;
@property({ attribute: "hide-name", type: Boolean }) public hideName = false;

View File

@ -115,14 +115,24 @@ export class HuiViewBackgroundEditor extends LitElement {
};
}
background = {
transparency: 100,
alignment: "center",
size: "auto",
repeat: "no-repeat",
attachment: "scroll",
...background,
};
if (!background) {
background = {
transparency: 33,
alignment: "center",
size: "cover",
repeat: "repeat",
attachment: "fixed",
};
} else {
background = {
transparency: 100,
alignment: "center",
size: "cover",
repeat: "no-repeat",
attachment: "scroll",
...background,
};
}
return html`
<ha-form

View File

@ -52,8 +52,8 @@ export class HUIViewBackground extends LitElement {
background?: string | LovelaceViewBackgroundConfig
) {
if (typeof background === "object" && background.image) {
const size = background.size ?? "auto";
const alignment = background.alignment ?? "center";
const size = background.size ?? "cover";
const repeat = background.repeat ?? "no-repeat";
return `${alignment} / ${size} ${repeat} url('${background.image}')`;
}

View File

@ -88,7 +88,7 @@ class HaMfaModuleSetupFlow extends LitElement {
</div>`
: html`${this._step.type === "abort"
? html` <ha-markdown
allowsvg
allow-svg
breaks
.content=${this.hass.localize(
`component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}`
@ -103,7 +103,7 @@ class HaMfaModuleSetupFlow extends LitElement {
</p>`
: this._step.type === "form"
? html`<ha-markdown
allowsvg
allow-svg
breaks
.content=${this.hass.localize(
`component.auth.mfa_setup.${

View File

@ -936,7 +936,7 @@
"delete": "Delete {count}",
"deleting": "Deleting {count}",
"tip_media_storage": "[%key:ui::panel::config::tips::media_storage%]",
"tip_storage_panel": "[%key:ui::panel::config::storage::caption%]"
"tip_storage_panel": "storage"
},
"class": {
"album": "Album",
@ -2685,7 +2685,8 @@
"picker": {
"headers": {
"url": "URL",
"type": "Type"
"type": "Type",
"delete": "Delete"
},
"no_resources": "No resources",
"add_resource": "Add resource"
@ -2957,8 +2958,8 @@
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
"edit_category": "Edit category",
"assign_category": "Assign category",
"no_category_support": "You can't assign an category to this automation",
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
"no_category_support": "You can't assign a category to this automation",
"no_category_entity_reg": "To assign a category to an automation it needs to have a unique ID.",
"search": "Search {number} automations",
"headers": {
"toggle": "Enable/disable",
@ -3717,7 +3718,8 @@
"add_description": "Add description",
"add_icon": "Add icon",
"add_category": "Add category",
"add_labels": "Add labels"
"add_labels": "Add labels",
"add_area": "Add area"
}
},
"trace": {
@ -4117,6 +4119,7 @@
"try": "Try",
"dialog": {
"header": "Try text-to-speech",
"message": "Message",
"example_message": "Hello {name}, you can play any text on any supported media player!",
"target": "Target",
"target_browser": "Browser",

View File

@ -1234,10 +1234,10 @@ __metadata:
languageName: node
linkType: hard
"@braintree/sanitize-url@npm:7.1.0":
version: 7.1.0
resolution: "@braintree/sanitize-url@npm:7.1.0"
checksum: 10/b25cc5358bedfd97d8378d23ab43493e56a805bd82fdb092088bdd9db6aa3f6c32859d36526f570fb2c67a5a4f9ce579aacd52c3872db4285e4c34fb9947dfc0
"@braintree/sanitize-url@npm:7.1.1":
version: 7.1.1
resolution: "@braintree/sanitize-url@npm:7.1.1"
checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0
languageName: node
linkType: hard
@ -9117,7 +9117,7 @@ __metadata:
"@babel/preset-env": "npm:7.26.0"
"@babel/preset-typescript": "npm:7.26.0"
"@babel/runtime": "npm:7.26.0"
"@braintree/sanitize-url": "npm:7.1.0"
"@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.17.0"
"@codemirror/autocomplete": "npm:6.18.4"
"@codemirror/commands": "npm:6.7.1"
@ -9296,7 +9296,7 @@ __metadata:
tsparticles-engine: "npm:2.12.0"
tsparticles-preset-links: "npm:2.12.0"
typescript: "npm:5.7.2"
ua-parser-js: "npm:1.0.39"
ua-parser-js: "npm:1.0.40"
vis-data: "npm:7.1.9"
vis-network: "npm:9.1.9"
vitest: "npm:2.1.8"
@ -14440,12 +14440,12 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:1.0.39":
version: 1.0.39
resolution: "ua-parser-js@npm:1.0.39"
"ua-parser-js@npm:1.0.40":
version: 1.0.40
resolution: "ua-parser-js@npm:1.0.40"
bin:
ua-parser-js: script/cli.js
checksum: 10/dd4026b6ece8a34a0d39b6de5542154c4506077d8def8647a300a29e1b3ffa0e23f5c8eeeb8101df6162b7b3eb3597d0b4adb031ae6104cbdb730d6ebc07f3c0
checksum: 10/7fced5f74ed570c83addffd4d367888d90c58803ff4bdd4a7b04b3f01d293263b8605e92ac560eb1c6a201ef3b11fcc46f3dbcbe764fbe54974924d542bc0135
languageName: node
linkType: hard