mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 16:38:31 +00:00
fix v1.7.3: PDF upload (FileReader.readAsDataURL instead of fragile btoa-chunking; accumulate + attach only if dialog still open) and edit dialogs no longer close on backdrop click / spurious clicks (marker/space/room require explicit Cancel; info-card keeps click-outside)
This commit is contained in:
@@ -10,7 +10,7 @@ PLANS_DIR = "houseplan/plans" # относительно каталога ко
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.7.2"
|
VERSION = "1.7.3"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -15,5 +15,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
|
"issue_tracker": "https://github.com/justbusiness/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"version": "1.7.2"
|
"version": "1.7.3"
|
||||||
}
|
}
|
||||||
Vendored
+46
-46
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.7.3 — 2026-07-04 (фиксы диалога устройства)
|
||||||
|
- PDF-инструкции не загружались: ручной base64 (`btoa(String.fromCharCode(...subarray))`) падал
|
||||||
|
на реальных файлах (RangeError при спреде больших массивов), а при закрытии диалога во время
|
||||||
|
await бросал null-доступ. Заменено на `FileReader.readAsDataURL` (надёжно для любого размера),
|
||||||
|
результаты аккумулируются и добавляются, только если диалог ещё открыт (без исключений).
|
||||||
|
- Диалог редактирования устройства/пространства/комнаты закрывался по клику мимо и «сам»
|
||||||
|
(случайный клик по фон-оверлею, в т.ч. после закрытия системного выбора файла). Убрано
|
||||||
|
закрытие по клику на фон у диалогов-редакторов — только явные Отмена/Сохранить/Esc.
|
||||||
|
Инфо-карточка (только чтение) по-прежнему закрывается кликом мимо.
|
||||||
|
- Проверено на живом HA: PDF прикрепляется; клик по фону и обновление данных диалог не закрывают.
|
||||||
|
|
||||||
## v1.7.2 — 2026-07-04 (фикс мобильного «ошибка конфигурации»)
|
## v1.7.2 — 2026-07-04 (фикс мобильного «ошибка конфигурации»)
|
||||||
- ПРИЧИНА: карточка подключалась только через `add_extra_js_url` (extra_module_url), загрузку
|
- ПРИЧИНА: карточка подключалась только через `add_extra_js_url` (extra_module_url), загрузку
|
||||||
которого фронтенд НЕ дожидается — на холодном старте мобильного приложения дашборд рисовался
|
которого фронтенд НЕ дожидается — на холодном старте мобильного приложения дашборд рисовался
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
|
"description": "Interactive house plan Lovelace card for Home Assistant (dacha Kirillovskoe)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+29
-14
@@ -13,7 +13,7 @@ import {
|
|||||||
} from './logic';
|
} from './logic';
|
||||||
import './editor';
|
import './editor';
|
||||||
|
|
||||||
const CARD_VERSION = '1.7.2';
|
const CARD_VERSION = '1.7.3';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
|
const NORM_W = 1000; // ширина рендер-пространства для нормированных конфигов
|
||||||
|
|
||||||
@@ -1202,28 +1202,43 @@ class HouseplanCard extends LitElement {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** base64 файла через FileReader — надёжно для любого размера (без спреда больших массивов). */
|
||||||
|
private _fileToBase64(file: File): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const res = String(reader.result || '');
|
||||||
|
const comma = res.indexOf(',');
|
||||||
|
resolve(comma >= 0 ? res.slice(comma + 1) : res);
|
||||||
|
};
|
||||||
|
reader.onerror = () => reject(reader.error || new Error('read error'));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _pickMarkerFiles(ev: Event): Promise<void> {
|
private async _pickMarkerFiles(ev: Event): Promise<void> {
|
||||||
const input = ev.target as HTMLInputElement;
|
const input = ev.target as HTMLInputElement;
|
||||||
const files = input.files ? [...input.files] : [];
|
const files = input.files ? [...input.files] : [];
|
||||||
|
input.value = '';
|
||||||
if (!files.length || !this._markerDialog) return;
|
if (!files.length || !this._markerDialog) return;
|
||||||
const mid = this._markerDialog.devId || 'new';
|
const mid = this._markerDialog.devId || 'new';
|
||||||
|
const uploaded: PdfRef[] = [];
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
const buf = new Uint8Array(await file.arrayBuffer());
|
const data = await this._fileToBase64(file);
|
||||||
let bin = '';
|
|
||||||
for (let i = 0; i < buf.length; i += 32768) bin += String.fromCharCode(...buf.subarray(i, i + 32768));
|
|
||||||
const resp = await this.hass.callWS({
|
const resp = await this.hass.callWS({
|
||||||
type: 'houseplan/file/set', marker_id: mid, filename: file.name, data: btoa(bin),
|
type: 'houseplan/file/set', marker_id: mid, filename: file.name, data,
|
||||||
});
|
});
|
||||||
this._markerDialog = {
|
uploaded.push({ name: resp.name || file.name, url: resp.url });
|
||||||
...this._markerDialog!,
|
|
||||||
pdfs: [...this._markerDialog!.pdfs, { name: resp.name || file.name, url: resp.url }],
|
|
||||||
};
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._showToast('Файл не загружен: ' + (e?.message || e));
|
this._showToast('Файл «' + file.name + '» не загружен: ' + (e?.code || e?.message || e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.value = '';
|
// диалог мог быть переоткрыт/закрыт за время загрузки — добавляем, только если он ещё открыт
|
||||||
|
if (uploaded.length && this._markerDialog) {
|
||||||
|
this._markerDialog = { ...this._markerDialog, pdfs: [...this._markerDialog.pdfs, ...uploaded] };
|
||||||
|
if (uploaded.length) this._showToast('Прикреплено файлов: ' + uploaded.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeMarkerPdf(url: string): void {
|
private _removeMarkerPdf(url: string): void {
|
||||||
@@ -1732,7 +1747,7 @@ class HouseplanCard extends LitElement {
|
|||||||
if (k === 'device') return this.hass.devices[ref]?.name_by_user || this.hass.devices[ref]?.name || ref;
|
if (k === 'device') return this.hass.devices[ref]?.name_by_user || this.hass.devices[ref]?.name || ref;
|
||||||
return this.hass.states[ref]?.attributes?.friendly_name || ref;
|
return this.hass.states[ref]?.attributes?.friendly_name || ref;
|
||||||
})();
|
})();
|
||||||
return html`<div class="menuwrap dialogwrap" @click=${() => (this._markerDialog = null)}>
|
return html`<div class="menuwrap dialogwrap" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="dialog wide" @click=${(e: Event) => e.stopPropagation()}>
|
<div class="dialog wide" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="hd"><ha-icon icon="mdi:shape-plus"></ha-icon>
|
<div class="hd"><ha-icon icon="mdi:shape-plus"></ha-icon>
|
||||||
${d.devId ? 'Устройство на плане' : 'Новое устройство'}</div>
|
${d.devId ? 'Устройство на плане' : 'Новое устройство'}</div>
|
||||||
@@ -1830,7 +1845,7 @@ class HouseplanCard extends LitElement {
|
|||||||
|
|
||||||
private _renderSpaceDialog(): TemplateResult {
|
private _renderSpaceDialog(): TemplateResult {
|
||||||
const d = this._spaceDialog!;
|
const d = this._spaceDialog!;
|
||||||
return html`<div class="menuwrap dialogwrap" @click=${() => (this._spaceDialog = null)}>
|
return html`<div class="menuwrap dialogwrap" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="hd"><ha-icon icon="mdi:floor-plan"></ha-icon>
|
<div class="hd"><ha-icon icon="mdi:floor-plan"></ha-icon>
|
||||||
${d.mode === 'create' ? 'Новое пространство' : 'Пространство'}</div>
|
${d.mode === 'create' ? 'Новое пространство' : 'Пространство'}</div>
|
||||||
@@ -1871,7 +1886,7 @@ class HouseplanCard extends LitElement {
|
|||||||
|
|
||||||
private _renderRoomDialog(): TemplateResult {
|
private _renderRoomDialog(): TemplateResult {
|
||||||
const areas = this._freeAreas;
|
const areas = this._freeAreas;
|
||||||
return html`<div class="menuwrap dialogwrap" @click=${this._roomDialogCancel}>
|
return html`<div class="menuwrap dialogwrap" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
<div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
|
||||||
<div class="hd"><ha-icon icon="mdi:floor-plan"></ha-icon>Новая комната</div>
|
<div class="hd"><ha-icon icon="mdi:floor-plan"></ha-icon>Новая комната</div>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
|||||||
Reference in New Issue
Block a user