@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56",
|
||||
"fingerprint": "aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 280678,
|
||||
"initialViewGzipBytes": 281796,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/fr-BmVscop_.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
],
|
||||
"lazyGzipBytes": 197394,
|
||||
"lazyGzipBytes": 197638,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 143764,
|
||||
"lazyEditorGzipBytes": 143905,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 13790,
|
||||
"lazyOnboardingGzipBytes": 13792,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/fr-BmVscop_.js"
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 46912,
|
||||
"lazyLocaleGzipBytes": 47013,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"sha256": "88ef8c9dc5546644333250bd1c74880309cbad31cb7134d8a1fd80114288c865",
|
||||
"path": "houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"sha256": "15bf3fd17239abfd64b336b191de40d1d4cc2cb812dc9e0f9f2c87caa845f03d",
|
||||
"rawBytes": 20636,
|
||||
"gzipBytes": 7072,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-CQInYVzK.js",
|
||||
"sha256": "ccf0bcc218d15199ec186ee04e4de4805eeb525e053d483887a0c146c231c8f8",
|
||||
"rawBytes": 78176,
|
||||
"gzipBytes": 23682,
|
||||
"path": "houseplan-assets/de-CV45z-ti.js",
|
||||
"sha256": "911c01b0d7e0a885f703403982923e4167acb8abc4da98a922cb9e6742240db0",
|
||||
"rawBytes": 78310,
|
||||
"gzipBytes": 23728,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DxZvDwp3.js",
|
||||
"sha256": "99a5e16cda136b8c02e62c87d8ed2912ad05cb8b3a4a344cf0bd6636ad5e9636",
|
||||
"path": "houseplan-assets/editor-DFZF1CWt.js",
|
||||
"sha256": "0f09c8de0f2538760376d145222d6c0dbcebb43163fec11259fa2d861ba51158",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1581,
|
||||
"gzipBytes": 1584,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-BmVscop_.js",
|
||||
"sha256": "0c53056508a989f0db01ccd66ca809a93853a2a7477cb779434d1caaf6bfa2ed",
|
||||
"rawBytes": 80344,
|
||||
"gzipBytes": 23230,
|
||||
"path": "houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"sha256": "bd7b7f20e4af5c371fc8165e6226fd2122c4734ce4cacb783765d40de2f59381",
|
||||
"rawBytes": 80482,
|
||||
"gzipBytes": 23285,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Dhz45m9k.js",
|
||||
"sha256": "17361b7baa37e7e989dc3caf8b6d412b829bc02b2724f84757abacf8ad4c5b7b",
|
||||
"rawBytes": 991515,
|
||||
"gzipBytes": 279883,
|
||||
"path": "houseplan-assets/houseplan-card-CaTFHDy3.js",
|
||||
"sha256": "5407c26bc8bd764f7d97e197cac43dc88dc1d4b44681f60532dbfbc0df98639f",
|
||||
"rawBytes": 996305,
|
||||
"gzipBytes": 280999,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/fr-BmVscop_.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"sha256": "bdb3b6dcff0495ef15b59dbe69feef693b00733991b57555623c004e4793d0d9",
|
||||
"rawBytes": 522799,
|
||||
"gzipBytes": 135111,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"sha256": "a7f87e8bf473a5549841410f09e3105990e85d9fff948bb29824e6255a812212",
|
||||
"rawBytes": 524026,
|
||||
"gzipBytes": 135249,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js",
|
||||
"sha256": "4933a98e4125964dd6ade0645b355857121855c7b2260fb472d91ef68c99c161",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js",
|
||||
"sha256": "1bb9b33c9395fa9f2c85cb534e4e92354a0563efc45522e078ffe9da6fdc0c14",
|
||||
"rawBytes": 27120,
|
||||
"gzipBytes": 6718,
|
||||
"gzipBytes": 6720,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "1fc4ad672293202be7cbc843c70c72fc009c5ff8be251189a4901ad6326bd1df",
|
||||
"sha256": "c45de5b9d825097729b03b8ee0063ca7424236137b63a44bdec36aada307b0a0",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"gzipBytes": 797,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";import{b as e,l as o,t,d7 as s,A as a,d8 as i,d9 as l,da as n,E as r,c}from"./houseplan-card-Dhz45m9k.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";import{b as e,l as o,t,d7 as s,A as a,d8 as i,d9 as l,da as n,E as r,c}from"./houseplan-card-CaTFHDy3.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";import{l as t,ar as s,A as e,t as o,c as a,bV as l,bW as i,bX as h,bY as c,ao as n,bZ as p,c2 as r,b_ as _,b$ as d,c0 as u,c1 as g,cX as m,an as b,ap as $,cY as v}from"./houseplan-card-Dhz45m9k.js";import{i as f,c as y,e as w,r as D,a as S,b as k,s as C,t as M}from"./backdrop-pick-CnZSQy0i.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";class I{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";import{l as t,ar as s,A as e,t as o,c as a,bV as l,bW as i,bX as h,bY as c,ao as n,bZ as p,c2 as r,b_ as _,b$ as d,c0 as u,c1 as g,cX as m,an as b,ap as $,cY as v}from"./houseplan-card-CaTFHDy3.js";import{i as f,c as y,e as w,r as D,a as S,b as k,s as C,t as M}from"./backdrop-pick-B6HYqsoo.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";class I{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,r="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:p(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:r,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=r(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...f(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:d,tempMax:_,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await y(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await w(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return D(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
|
||||
${s.map(s=>{return a`
|
||||
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";try{await import("./houseplan-assets/houseplan-card-Dhz45m9k.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";try{await import("./houseplan-assets/houseplan-card-CaTFHDy3.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch();
|
||||
const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const wait = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const settle = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const idle = async () => {
|
||||
const started = performance.now();
|
||||
while (c._devicePositionBusy && performance.now() - started < 1500) await wait(10);
|
||||
await c.updateComplete;
|
||||
};
|
||||
|
||||
[...sr().querySelectorAll('.modetab')][1].click();
|
||||
await settle();
|
||||
const deviceNode = () => sr().querySelector('.dev:not(.ha-disabled)');
|
||||
let node = deviceNode();
|
||||
const deviceId = node?.getAttribute('data-id');
|
||||
const device = c._devices.find((candidate) => candidate.id === deviceId);
|
||||
if (!node || !device) return { fixtureReady: false };
|
||||
out.fixtureReady = true;
|
||||
|
||||
const originalCallWs = c.hass.callWS;
|
||||
let revision = 20;
|
||||
let serverLayout = structuredClone(c._layout);
|
||||
const writes = [];
|
||||
let failNextUpdate = false;
|
||||
let failNextDelete = false;
|
||||
c.hass = { ...c.hass, callWS: async (message) => {
|
||||
if (message.type === 'houseplan/layout/get') {
|
||||
return { layout: structuredClone(serverLayout), rev: revision };
|
||||
}
|
||||
if (message.type === 'houseplan/layout/update') {
|
||||
if (failNextUpdate) { failNextUpdate = false; throw new Error('update refused'); }
|
||||
writes.push({ type: 'update', id: message.device_id, pos: structuredClone(message.pos) });
|
||||
serverLayout = { ...serverLayout, [message.device_id]: structuredClone(message.pos) };
|
||||
return { rev: ++revision };
|
||||
}
|
||||
if (message.type === 'houseplan/layout/delete') {
|
||||
if (failNextDelete) { failNextDelete = false; throw new Error('delete refused'); }
|
||||
writes.push({ type: 'delete', id: message.device_id });
|
||||
delete serverLayout[message.device_id];
|
||||
return { rev: ++revision };
|
||||
}
|
||||
return originalCallWs(message);
|
||||
} };
|
||||
c._serverStorage = true;
|
||||
c._layoutRev = revision;
|
||||
|
||||
const fire = (target, type, x, y, pointerId = 74) => target.dispatchEvent(new PointerEvent(type, {
|
||||
pointerId, pointerType: 'mouse', button: 0, buttons: type === 'pointerup' ? 0 : 1,
|
||||
clientX: x, clientY: y, bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
const drag = async ({ dx = 80, cancel = false, cancelType = 'pointercancel', moves = 10, pointerId = 74 } = {}) => {
|
||||
node = deviceNode();
|
||||
const rect = node.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
fire(node, 'pointerdown', x, y, pointerId);
|
||||
for (let index = 1; index <= moves; index++) {
|
||||
fire(node, 'pointermove', x + dx * index / moves, y, pointerId);
|
||||
}
|
||||
const writesBeforeEnd = writes.length;
|
||||
fire(node, cancel ? cancelType : 'pointerup', x + dx, y, pointerId);
|
||||
await idle();
|
||||
return writesBeforeEnd;
|
||||
};
|
||||
const beginPreview = (dx, pointerId) => {
|
||||
node = deviceNode();
|
||||
const rect = node.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
fire(node, 'pointerdown', x, y, pointerId);
|
||||
fire(node, 'pointermove', x + dx, y, pointerId);
|
||||
return { node, x, y };
|
||||
};
|
||||
|
||||
const explicit = c._devicePlacementForCanvas(device, c._pos(device).x, c._pos(device).y);
|
||||
c._layout = { ...c._layout, [deviceId]: { ...explicit, k: 0, future: 'kept' } };
|
||||
serverLayout = structuredClone(c._layout);
|
||||
c._layoutContentFingerprint = '';
|
||||
c._devicePositionHistory.clear();
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
|
||||
const writesBeforeRelease = await drag();
|
||||
out.previewDoesNotPersist = writesBeforeRelease === 0;
|
||||
out.oneDragOneWrite = writes.length === 1 && writes[0].type === 'update';
|
||||
out.oneDragOneCommand = c._devicePositionHistory.size === 1;
|
||||
out.futureFieldsSurviveDrag = c._layout[deviceId].k === 0
|
||||
&& c._layout[deviceId].future === 'kept';
|
||||
out.undoEnabled = !sr().querySelector('[data-device-position-history="undo"]').disabled;
|
||||
|
||||
const afterDrag = structuredClone(c._layout[deviceId]);
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await idle();
|
||||
out.undoRestoresExactStart = c._layout[deviceId].x === explicit.x
|
||||
&& c._layout[deviceId].y === explicit.y
|
||||
&& c._layout[deviceId].k === 0
|
||||
&& c._devicePositionHistory.canRedo;
|
||||
sr().querySelector('[data-device-position-history="redo"]').click();
|
||||
await idle();
|
||||
out.redoRestoresExactEnd = JSON.stringify(c._layout[deviceId]) === JSON.stringify(afterDrag);
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'z', code: 'KeyZ', ctrlKey: true, bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
await idle();
|
||||
out.keyboardUndoWorks = c._devicePositionHistory.canRedo
|
||||
&& c._layout[deviceId].x === explicit.x && c._layout[deviceId].y === explicit.y;
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'z', code: 'KeyZ', ctrlKey: true, shiftKey: true,
|
||||
bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
await idle();
|
||||
out.keyboardRedoWorks = JSON.stringify(c._layout[deviceId]) === JSON.stringify(afterDrag);
|
||||
|
||||
// Escape and an Undo request during preview abort only the uncommitted drag.
|
||||
const beforeEscape = structuredClone(c._layout[deviceId]);
|
||||
const writesBeforeEscape = writes.length;
|
||||
beginPreview(95, 741);
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Escape', code: 'Escape', bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
await c.updateComplete;
|
||||
out.escapeAbortsWithoutWrite = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeEscape)
|
||||
&& writes.length === writesBeforeEscape && c._deviceDrag === null;
|
||||
const historySizeBeforePreviewUndo = c._devicePositionHistory.size;
|
||||
beginPreview(105, 742);
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await c.updateComplete;
|
||||
out.undoDuringDragOnlyAborts = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeEscape)
|
||||
&& writes.length === writesBeforeEscape
|
||||
&& c._devicePositionHistory.size === historySizeBeforePreviewUndo
|
||||
&& c._deviceDrag === null;
|
||||
|
||||
// Leaving the editor is another abort boundary; re-enter for the remaining cases.
|
||||
beginPreview(115, 743);
|
||||
[...sr().querySelectorAll('.modetab')][0].click();
|
||||
await settle();
|
||||
out.modeSwitchAbortsWithoutWrite = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeEscape)
|
||||
&& writes.length === writesBeforeEscape && c._deviceDrag === null;
|
||||
[...sr().querySelectorAll('.modetab')][1].click();
|
||||
await settle();
|
||||
|
||||
// A no-op after Undo must not destroy the redo branch.
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await idle();
|
||||
const redoName = c._devicePositionHistory.redoName;
|
||||
const writesBeforeNoop = writes.length;
|
||||
await drag({ dx: 0, moves: 0, pointerId: 75 });
|
||||
out.noopKeepsRedo = c._devicePositionHistory.redoName === redoName
|
||||
&& writes.length === writesBeforeNoop;
|
||||
sr().querySelector('[data-device-position-history="redo"]').click();
|
||||
await idle();
|
||||
|
||||
// Cancellation restores the raw before-state and performs no final write.
|
||||
const beforeCancel = structuredClone(c._layout[deviceId]);
|
||||
const writesBeforeCancel = writes.length;
|
||||
await drag({ dx: 120, cancel: true, pointerId: 76 });
|
||||
out.cancelRestoresWithoutWrite = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeCancel)
|
||||
&& writes.length === writesBeforeCancel;
|
||||
await drag({ dx: 120, cancel: true, cancelType: 'lostpointercapture', pointerId: 761 });
|
||||
out.lostCaptureRestoresWithoutWrite = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeCancel)
|
||||
&& writes.length === writesBeforeCancel;
|
||||
|
||||
// A second pointer invalidates the first single-pointer transaction.
|
||||
node = deviceNode();
|
||||
let rect = node.getBoundingClientRect();
|
||||
let x = rect.left + rect.width / 2;
|
||||
let y = rect.top + rect.height / 2;
|
||||
fire(node, 'pointerdown', x, y, 762);
|
||||
fire(node, 'pointermove', x + 100, y, 762);
|
||||
fire(node, 'pointerdown', x, y, 763);
|
||||
await c.updateComplete;
|
||||
out.secondPointerAbortsWithoutWrite = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeCancel)
|
||||
&& writes.length === writesBeforeCancel && c._deviceDrag === null;
|
||||
|
||||
// An auto-positioned marker round-trips through delete/update.
|
||||
c._devicePositionHistory.clear();
|
||||
delete c._layout[deviceId];
|
||||
delete serverLayout[deviceId];
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
await drag({ dx: 90, pointerId: 77 });
|
||||
const autoAfter = structuredClone(c._layout[deviceId]);
|
||||
const beforeAutoUndo = writes.length;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await idle();
|
||||
out.autoUndoDeletesExplicitPlacement = !(deviceId in c._layout)
|
||||
&& writes[beforeAutoUndo]?.type === 'delete';
|
||||
sr().querySelector('[data-device-position-history="redo"]').click();
|
||||
await idle();
|
||||
out.autoRedoRestoresPlacement = JSON.stringify(c._layout[deviceId]) === JSON.stringify(autoAfter)
|
||||
&& writes.at(-1)?.type === 'update';
|
||||
|
||||
// Failed final update never enters history; failed Undo restores stack direction.
|
||||
c._devicePositionHistory.clear();
|
||||
const beforeFailedDrag = structuredClone(c._layout[deviceId]);
|
||||
failNextUpdate = true;
|
||||
await drag({ dx: 110, pointerId: 78 });
|
||||
out.failedDragRollsBack = JSON.stringify(c._layout[deviceId]) === JSON.stringify(beforeFailedDrag)
|
||||
&& !c._devicePositionHistory.canUndo;
|
||||
delete c._layout[deviceId];
|
||||
delete serverLayout[deviceId];
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
await drag({ dx: 100, pointerId: 79 });
|
||||
const beforeFailedDelete = structuredClone(c._layout[deviceId]);
|
||||
failNextDelete = true;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await idle();
|
||||
out.failedUndoRestoresStackDirection = JSON.stringify(c._layout[deviceId])
|
||||
=== JSON.stringify(beforeFailedDelete)
|
||||
&& c._devicePositionHistory.canUndo && !c._devicePositionHistory.canRedo;
|
||||
|
||||
// Native text history stays native even with an available command.
|
||||
const input = document.createElement('input');
|
||||
sr().append(input);
|
||||
const sizeBeforeInput = c._devicePositionHistory.size;
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'z', code: 'KeyZ', ctrlKey: true, bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
await wait();
|
||||
out.nativeInputHistoryNotIntercepted = c._devicePositionHistory.size === sizeBeforeInput;
|
||||
input.remove();
|
||||
|
||||
// Same-content reload is an own/reconnect echo; different content is remote authority.
|
||||
serverLayout = structuredClone(c._layout);
|
||||
await c._reloadLayoutOnly();
|
||||
out.sameContentReloadKeepsHistory = c._devicePositionHistory.canUndo;
|
||||
serverLayout = {
|
||||
...serverLayout,
|
||||
[deviceId]: { ...serverLayout[deviceId], x: serverLayout[deviceId].x + 0.01 },
|
||||
};
|
||||
await c._reloadLayoutOnly();
|
||||
out.remoteContentClearsHistory = !c._devicePositionHistory.canUndo
|
||||
&& !c._devicePositionHistory.canRedo;
|
||||
|
||||
// A valid command owns its original space and makes the result visible there.
|
||||
const otherDevice = c._devices.find((candidate) => candidate.space !== device.space
|
||||
&& candidate.bindingStatus?.kind !== 'ha_disabled');
|
||||
if (otherDevice) {
|
||||
const otherBefore = { s: otherDevice.space, x: 0.55, y: 0.55 };
|
||||
const otherAfter = { s: otherDevice.space, x: 0.6, y: 0.55 };
|
||||
c._layout = { ...c._layout, [otherDevice.id]: structuredClone(otherAfter) };
|
||||
serverLayout = structuredClone(c._layout);
|
||||
c._layoutContentFingerprint = '';
|
||||
c._devicePositionHistory.push({
|
||||
name: 'other-space move',
|
||||
before: { deviceId: otherDevice.id, spaceId: otherDevice.space, placement: otherBefore },
|
||||
after: { deviceId: otherDevice.id, spaceId: otherDevice.space, placement: otherAfter },
|
||||
});
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await idle();
|
||||
await settle();
|
||||
const observedOther = c._layout[otherDevice.id];
|
||||
out.otherSpaceUndoIsVisible = c._space === otherDevice.space
|
||||
&& observedOther?.s === otherBefore.s
|
||||
&& observedOther?.x === otherBefore.x
|
||||
&& observedOther?.y === otherBefore.y;
|
||||
c._commitSpace(device.space);
|
||||
await settle();
|
||||
} else {
|
||||
out.otherSpaceUndoIsVisible = false;
|
||||
}
|
||||
|
||||
// Stale commands fail closed instead of recreating a deleted/rebound/disabled marker.
|
||||
const staleCommand = (id, spaceId) => ({
|
||||
name: 'stale move',
|
||||
before: { deviceId: id, spaceId, placement: { s: spaceId, x: 0.4, y: 0.4 } },
|
||||
after: { deviceId: id, spaceId, placement: { s: spaceId, x: 0.45, y: 0.4 } },
|
||||
});
|
||||
const writesBeforeStale = writes.length;
|
||||
c._devicePositionHistory.clear();
|
||||
c._devicePositionHistory.push(staleCommand('removed-device', device.space));
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await c.updateComplete;
|
||||
out.deletedDeviceCommandFailsClosed = !c._devicePositionHistory.canUndo
|
||||
&& !c._devicePositionHistory.canRedo && writes.length === writesBeforeStale;
|
||||
|
||||
c._devicePositionHistory.push(staleCommand(device.id, 'garden'));
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await c.updateComplete;
|
||||
out.reboundDeviceCommandFailsClosed = !c._devicePositionHistory.canUndo
|
||||
&& !c._devicePositionHistory.canRedo && writes.length === writesBeforeStale;
|
||||
|
||||
const devicesBeforeDisable = c._devices;
|
||||
c._devices = devicesBeforeDisable.map((candidate) => candidate.id === device.id
|
||||
? { ...candidate, bindingStatus: { kind: 'ha_disabled' } } : candidate);
|
||||
c._devicePositionHistory.push(staleCommand(device.id, device.space));
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
sr().querySelector('[data-device-position-history="undo"]').click();
|
||||
await c.updateComplete;
|
||||
out.disabledDeviceCommandFailsClosed = !c._devicePositionHistory.canUndo
|
||||
&& !c._devicePositionHistory.canRedo && writes.length === writesBeforeStale;
|
||||
c._devices = devicesBeforeDisable;
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
checkAll(res, {});
|
||||
await finish(browser, res);
|
||||
@@ -158,7 +158,11 @@ const res = await page.evaluate(async () => {
|
||||
out.devBar = !!sr().querySelector('.editbar.devbar');
|
||||
const deviceToolbarButtons = [...sr().querySelectorAll('.editbar.devbar .btn:not(.barclose)')];
|
||||
const addDeviceButton = deviceToolbarButtons[0];
|
||||
out.devBarBtns = deviceToolbarButtons.length === 3;
|
||||
out.devBarBtns = deviceToolbarButtons.length === 5;
|
||||
out.deviceHistoryControlsArePersistent = !!sr().querySelector('[data-device-position-history="undo"]')
|
||||
&& !!sr().querySelector('[data-device-position-history="redo"]')
|
||||
&& sr().querySelector('[data-device-position-history="undo"]').disabled
|
||||
&& sr().querySelector('[data-device-position-history="redo"]').disabled;
|
||||
out.addDeviceShortcutUsesNativeKeyboardSemantics = addDeviceButton?.tagName === 'BUTTON'
|
||||
&& addDeviceButton.disabled === false;
|
||||
out.addDeviceShortcut = addDeviceButton?.textContent.trim() === c._t('devbar.add')
|
||||
|
||||
@@ -71,9 +71,22 @@ const out = await page.evaluate(async () => {
|
||||
bubbles: true, ...extra,
|
||||
});
|
||||
};
|
||||
c._drag = { id: dev.id, sx: 400, sy: 400, ox: p0.x, oy: p0.y, moved: false };
|
||||
c._pointerMove(mkDelta(137.7 - p0.x + OFF, 251.3 - p0.y + OFF), dev);
|
||||
c._drag = null;
|
||||
const devNode = sr().querySelector(`.dev[data-id="${dev.id}"]`);
|
||||
const devRect = devNode.getBoundingClientRect();
|
||||
const stageRect = sr().querySelector('.stage').getBoundingClientRect();
|
||||
const deviceView = c._viewOr(c._baseVb());
|
||||
const startX = devRect.left + devRect.width / 2;
|
||||
const startY = devRect.top + devRect.height / 2;
|
||||
const dxPx = ((137.7 - p0.x + OFF) / deviceView.w) * stageRect.width;
|
||||
const dyPx = ((251.3 - p0.y + OFF) / deviceView.h) * stageRect.height;
|
||||
const devicePointer = (type, x, y, buttons) => devNode.dispatchEvent(new PointerEvent(type, {
|
||||
pointerId: 174, pointerType: 'mouse', button: 0, buttons,
|
||||
clientX: x, clientY: y, bubbles: true, composed: true, cancelable: true,
|
||||
}));
|
||||
devicePointer('pointerdown', startX, startY, 1);
|
||||
devicePointer('pointermove', startX + dxPx, startY + dyPx, 1);
|
||||
devicePointer('pointerup', startX + dxPx, startY + dyPx, 0);
|
||||
while (c._devicePositionBusy) await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
o.deviceLandsOnANode = onGridN(c._layout[dev.id].x) && onGridN(c._layout[dev.id].y);
|
||||
|
||||
// ---- 1c) a ROOM LABEL too ---------------------------------------------
|
||||
|
||||
@@ -141,17 +141,17 @@ Object.assign(out, await page.evaluate(async () => {
|
||||
const v0 = { ...c._viewOr(c._baseVb()) };
|
||||
const r = dev.getBoundingClientRect();
|
||||
fire(dev, 'pointerdown', 22, r.left + r.width / 2, r.top + r.height / 2);
|
||||
o.deviceTakesThePointer = !!c._drag && c._panStart === null;
|
||||
o.deviceTakesThePointer = !!c._deviceDrag && c._panStart === null;
|
||||
fire(dev, 'pointermove', 22, r.left + r.width / 2 + 40, r.top + r.height / 2 + 30);
|
||||
c._stagePointerMove({ pointerId: 22, clientX: r.left + 60, clientY: r.top + 50 });
|
||||
const v1 = { ...c._viewOr(c._baseVb()) };
|
||||
o.deviceDragDoesNotPan = Math.abs(v1.x - v0.x) < 0.5 && Math.abs(v1.y - v0.y) < 0.5;
|
||||
o.deviceActuallyMoved = !!c._drag && c._drag.moved === true;
|
||||
o.deviceActuallyMoved = !!c._deviceDrag && c._deviceDrag.moved === true;
|
||||
fire(dev, 'pointerup', 22, r.left + r.width / 2 + 40, r.top + r.height / 2 + 30);
|
||||
// _pointerUp releases _drag on the next macrotask, and _stagePointerDown
|
||||
// _pointerUp releases the device drag before its serial final write, and _stagePointerDown
|
||||
// refuses to start a gesture while a device drag is live
|
||||
await new Promise((r2) => setTimeout(r2, 20));
|
||||
o.deviceDragReleased = c._drag === null;
|
||||
o.deviceDragReleased = c._deviceDrag === null;
|
||||
await c.updateComplete;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"fingerprint": "fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56",
|
||||
"fingerprint": "aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef",
|
||||
"entry": "houseplan-card.js",
|
||||
"initialViewFiles": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js",
|
||||
"houseplan-card.js"
|
||||
],
|
||||
"initialViewGzipBytes": 280678,
|
||||
"initialViewGzipBytes": 281796,
|
||||
"lazyFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/fr-BmVscop_.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
],
|
||||
"lazyGzipBytes": 197394,
|
||||
"lazyGzipBytes": 197638,
|
||||
"lazyEditorFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js"
|
||||
],
|
||||
"lazyEditorGzipBytes": 143764,
|
||||
"lazyEditorGzipBytes": 143905,
|
||||
"lazyOnboardingFiles": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
],
|
||||
"lazyOnboardingGzipBytes": 13790,
|
||||
"lazyOnboardingGzipBytes": 13792,
|
||||
"lazyLocaleFiles": [
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/fr-BmVscop_.js"
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js"
|
||||
],
|
||||
"lazyLocaleGzipBytes": 46912,
|
||||
"lazyLocaleGzipBytes": 47013,
|
||||
"files": [
|
||||
{
|
||||
"path": "houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"sha256": "88ef8c9dc5546644333250bd1c74880309cbad31cb7134d8a1fd80114288c865",
|
||||
"path": "houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"sha256": "15bf3fd17239abfd64b336b191de40d1d4cc2cb812dc9e0f9f2c87caa845f03d",
|
||||
"rawBytes": 20636,
|
||||
"gzipBytes": 7072,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/de-CQInYVzK.js",
|
||||
"sha256": "ccf0bcc218d15199ec186ee04e4de4805eeb525e053d483887a0c146c231c8f8",
|
||||
"rawBytes": 78176,
|
||||
"gzipBytes": 23682,
|
||||
"path": "houseplan-assets/de-CV45z-ti.js",
|
||||
"sha256": "911c01b0d7e0a885f703403982923e4167acb8abc4da98a922cb9e6742240db0",
|
||||
"rawBytes": 78310,
|
||||
"gzipBytes": 23728,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/editor-DxZvDwp3.js",
|
||||
"sha256": "99a5e16cda136b8c02e62c87d8ed2912ad05cb8b3a4a344cf0bd6636ad5e9636",
|
||||
"path": "houseplan-assets/editor-DFZF1CWt.js",
|
||||
"sha256": "0f09c8de0f2538760376d145222d6c0dbcebb43163fec11259fa2d861ba51158",
|
||||
"rawBytes": 3826,
|
||||
"gzipBytes": 1581,
|
||||
"gzipBytes": 1584,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/fr-BmVscop_.js",
|
||||
"sha256": "0c53056508a989f0db01ccd66ca809a93853a2a7477cb779434d1caaf6bfa2ed",
|
||||
"rawBytes": 80344,
|
||||
"gzipBytes": 23230,
|
||||
"path": "houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"sha256": "bd7b7f20e4af5c371fc8165e6226fd2122c4734ce4cacb783765d40de2f59381",
|
||||
"rawBytes": 80482,
|
||||
"gzipBytes": 23285,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-card-Dhz45m9k.js",
|
||||
"sha256": "17361b7baa37e7e989dc3caf8b6d412b829bc02b2724f84757abacf8ad4c5b7b",
|
||||
"rawBytes": 991515,
|
||||
"gzipBytes": 279883,
|
||||
"path": "houseplan-assets/houseplan-card-CaTFHDy3.js",
|
||||
"sha256": "5407c26bc8bd764f7d97e197cac43dc88dc1d4b44681f60532dbfbc0df98639f",
|
||||
"rawBytes": 996305,
|
||||
"gzipBytes": 280999,
|
||||
"isEntry": false,
|
||||
"imports": [],
|
||||
"dynamicImports": [
|
||||
"houseplan-assets/de-CQInYVzK.js",
|
||||
"houseplan-assets/editor-DxZvDwp3.js",
|
||||
"houseplan-assets/fr-BmVscop_.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js"
|
||||
"houseplan-assets/de-CV45z-ti.js",
|
||||
"houseplan-assets/editor-DFZF1CWt.js",
|
||||
"houseplan-assets/fr-Ck0o4CMs.js",
|
||||
"houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-CbsKtXpC.js",
|
||||
"sha256": "bdb3b6dcff0495ef15b59dbe69feef693b00733991b57555623c004e4793d0d9",
|
||||
"rawBytes": 522799,
|
||||
"gzipBytes": 135111,
|
||||
"path": "houseplan-assets/houseplan-editor-runtime-DrWOqPVN.js",
|
||||
"sha256": "a7f87e8bf473a5549841410f09e3105990e85d9fff948bb29824e6255a812212",
|
||||
"rawBytes": 524026,
|
||||
"gzipBytes": 135249,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-jtdU6h_l.js",
|
||||
"sha256": "4933a98e4125964dd6ade0645b355857121855c7b2260fb472d91ef68c99c161",
|
||||
"path": "houseplan-assets/houseplan-onboarding-runtime-wolz_s2E.js",
|
||||
"sha256": "1bb9b33c9395fa9f2c85cb534e4e92354a0563efc45522e078ffe9da6fdc0c14",
|
||||
"rawBytes": 27120,
|
||||
"gzipBytes": 6718,
|
||||
"gzipBytes": 6720,
|
||||
"isEntry": false,
|
||||
"imports": [
|
||||
"houseplan-assets/backdrop-pick-CnZSQy0i.js",
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/backdrop-pick-B6HYqsoo.js",
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
},
|
||||
{
|
||||
"path": "houseplan-card.js",
|
||||
"sha256": "1fc4ad672293202be7cbc843c70c72fc009c5ff8be251189a4901ad6326bd1df",
|
||||
"sha256": "c45de5b9d825097729b03b8ee0063ca7424236137b63a44bdec36aada307b0a0",
|
||||
"rawBytes": 1183,
|
||||
"gzipBytes": 795,
|
||||
"gzipBytes": 797,
|
||||
"isEntry": true,
|
||||
"imports": [
|
||||
"houseplan-assets/houseplan-card-Dhz45m9k.js"
|
||||
"houseplan-assets/houseplan-card-CaTFHDy3.js"
|
||||
],
|
||||
"dynamicImports": []
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";import{b as e,l as o,t,d7 as s,A as a,d8 as i,d9 as l,da as n,E as r,c}from"./houseplan-card-Dhz45m9k.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";import{b as e,l as o,t,d7 as s,A as a,d8 as i,d9 as l,da as n,E as r,c}from"./houseplan-card-CaTFHDy3.js";class h extends e{constructor(){super(...arguments),this._spaces=null,this._spacesLoading=!1,this._spacesAuthoritative=!1}setConfig(e){this._config=e}async _loadSpaces(){if(!this._spaces&&!this._spacesLoading&&this.hass){this._spacesLoading=!0;try{const e=await this.hass.callWS({type:"houseplan/config/get"});this._spaces=(e?.config?.spaces||[]).map(e=>({value:e.id,label:e.title||e.id})),this._spacesAuthoritative=!0}catch{this._spaces=[],this._spacesAuthoritative=!1}finally{this._spacesLoading=!1}}}get _lang(){return o(this.hass,this._config?.language)}get _floorToken(){const e=this._config?.floor;return"number"==typeof e?`__houseplan_yaml_floor_index__:${String(e)}`:null}get _formData(){const e={...this._config},o=this._floorToken;return o?e.floor=o:Object.prototype.hasOwnProperty.call(e,"floor")||(e.floor=""),e}get _schema(){const e=this._spaces||[],o=this._lang,a=[{value:"",label:t(o,"editor.floor_none")}],i=this._floorToken;i&&a.push({value:i,label:t(o,"editor.floor_index",{index:String(this._config?.floor)})});const l="string"==typeof this._config?.floor?this._config.floor:"";l&&!e.some(e=>e.value===l)&&a.push({value:l,label:l}),a.push(...e);const n="string"==typeof this._config?.default_floor?this._config.default_floor:"",r=[...e];return n&&!e.some(e=>e.value===n)&&r.unshift({value:n,label:n}),[{name:"title",selector:{text:{}}},{name:"floor",selector:{select:{mode:"dropdown",options:a}}},e.length?{name:"default_floor",selector:{select:{mode:"dropdown",options:r}}}:{name:"default_floor",selector:{text:{}}},{name:"language",selector:{select:{mode:"dropdown",options:s(t(o,"editor.lang_auto"),this._config?.language)}}},{name:"icon_size",selector:{number:{min:1,max:6,step:.1,mode:"box"}}},{name:"show_temperature",selector:{boolean:{}}},{name:"live_states",selector:{boolean:{}}},{name:"show_signal",selector:{boolean:{}}},{name:"kiosk",selector:{boolean:{}}},{name:"cycle",selector:{number:{min:0,max:3600,step:5,mode:"box"}}}]}render(){if(!this.hass||!this._config)return a;const e=i(this,l,o(this.hass,this._config.language));if("cold"===e)return n();if("warm"===e)return r;this._loadSpaces();const s=this._lang,h={title:t(s,"editor.title"),floor:t(s,"editor.floor"),default_floor:t(s,"editor.default_floor"),language:t(s,"editor.language"),icon_size:t(s,"editor.icon_size"),show_temperature:t(s,"editor.show_temperature"),live_states:t(s,"editor.live_states"),show_signal:t(s,"editor.show_signal"),kiosk:t(s,"editor.kiosk"),cycle:t(s,"editor.cycle")},f=this._schema,_=function(e,o,t){if(!t||null===o)return null;const s="string"==typeof e?.default_floor?e.default_floor:"";return!s||o.some(e=>e.value===s)?null:s}(this._config,this._spaces,this._spacesAuthoritative),d=e=>c`<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._formData}
|
||||
.schema=${e}
|
||||
@@ -1,4 +1,4 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";import{l as t,ar as s,A as e,t as o,c as a,bV as l,bW as i,bX as h,bY as c,ao as n,bZ as p,c2 as r,b_ as _,b$ as d,c0 as u,c1 as g,cX as m,an as b,ap as $,cY as v}from"./houseplan-card-Dhz45m9k.js";import{i as f,c as y,e as w,r as D,a as S,b as k,s as C,t as M}from"./backdrop-pick-CnZSQy0i.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";class I{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";import{l as t,ar as s,A as e,t as o,c as a,bV as l,bW as i,bX as h,bY as c,ao as n,bZ as p,c2 as r,b_ as _,b$ as d,c0 as u,c1 as g,cX as m,an as b,ap as $,cY as v}from"./houseplan-card-CaTFHDy3.js";import{i as f,c as y,e as w,r as D,a as S,b as k,s as C,t as M}from"./backdrop-pick-B6HYqsoo.js";const F=1e3,x=t=>{if(!t.trim())return null;const s=Number(t.replace(",","."));return Number.isFinite(s)?s:null},T="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";class I{constructor(t){this.host=t,this._toggleServerPlans=async()=>{const t=this.host._spaceDialog;if(t)if(t.pickSaved)this.host._spaceDialog={...t,pickSaved:!1};else{this.host._spaceDialog={...t,pickSaved:!0,savedBusy:!0};try{const t=await this.host.hass.callWS({type:"houseplan/plans/list"}),s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:t?.plans||[],savedBusy:!1})}catch(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,saved:[],savedBusy:!1}),this.host._showToast(this.host._t("toast.plans_list_failed",{err:this.host._errText(t)}))}}}}_help(l){const i=`${l}.aria`,h=t(this.host.hass,this.host._config?.language);return s(h,l)&&s(h,i)?a`<hp-help data-help-key=${l}
|
||||
.text=${o(h,l)} .ariaLabel=${o(h,i)}></hp-help>`:e}_openSpaceDialog(t,s){if(!this.host._serverStorage||!this.host._serverCfg)return void this.host._showToast(this.host._t("toast.integration_missing"));if("edit"===t){const e=this.host._serverCfg.spaces.find(t=>t.id===s);if(!e)return;const o=l(e),a=e.settings?.custom_fill&&"object"==typeof e.settings.custom_fill?i(e.settings.custom_fill):null,r="none"===o.fill?{...a||h,a:0}:a;return void(this.host._spaceDialog={mode:t,spaceId:s,title:e.title,planUrl:e.plan_url||null,planFile:null,source:e.plan_url?"file":"draw",showBorders:o.showBorders,showNames:o.showNames,zeroWallStyle:p(e),displayTouched:!0,hideDecor:o.hideDecor,hideOpenings:o.hideOpenings,roomColor:o.color,roomOpacity:o.opacity,fillMode:"none"===o.fill?"custom":o.fill,customFill:r,glowEnabled:o.glow,bgColor:o.bgColor,bgMode:"static"===e.settings?.bg_mode||"daynight"===e.settings?.bg_mode?e.settings.bg_mode:null,northDeg:n({},e.settings),sunRays:"boolean"==typeof e.settings?.sun_rays?e.settings.sun_rays:null,tempMin:o.tempMin,tempMax:o.tempMax,showLqi:o.showLqi??this.host._config?.show_signal??!0,cardFontScale:o.cardFontScale,labelTemp:o.labelTemp,labelHum:o.labelHum,labelLqi:o.labelLqi,labelLight:o.labelLight,cellCm:Number(e.cell_cm)>0?Number(e.cell_cm):5,cellCmInput:c(Number(e.cell_cm)>0?Number(e.cell_cm):5,this.host._imperial),cellCmTouched:!1,busy:!1})}const e=r(this.host._imperial);this.host._spaceDialog={mode:t,title:"",planUrl:null,planFile:null,...f(),hideDecor:!1,hideOpenings:!1,zeroWallStyle:"dashed",roomColor:g,roomOpacity:u,fillMode:"custom",customFill:{...h,a:0},glowEnabled:!0,bgColor:null,bgMode:"daynight",northDeg:null,sunRays:null,tempMin:d,tempMax:_,showLqi:this.host._config?.show_signal??!0,cardFontScale:1,labelTemp:!1,labelHum:!1,labelLqi:!1,labelLight:!1,cellCm:e,cellCmInput:c(e,this.host._imperial),cellCmTouched:!1,busy:!1}}async _pickPlanFile(t){const s=t.target,e=s.files?.[0];if(!e||!this.host._spaceDialog)return;s.value="";const o=await y(e);if("reject"===o.kind)return void this.host._showToast(this.host._t("toast.plan_formats"));if("guard"===o.kind)return void(this.host._backdropGuard=o.state);const a=await w(e,o.ext,e.name);this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:a})}_renderBackdropGuard(){return D(this.host,t=>{this.host._spaceDialog&&(this.host._spaceDialog={...this.host._spaceDialog,planFile:t})},()=>{this.host._backdropGuard=null},this.host.hass)??e}_useServerPlan(t){const s=this.host._spaceDialog;s&&(this.host._spaceDialog={...s,planUrl:t,planFile:null,pickSaved:!1,savedAspect:void 0},this.host._aspectJob=this._readPlanAspect(t))}async _readPlanAspect(t){for(let s=0;s<40;s++){const s=this.host._display(t);if(s){const e=await new Promise(t=>{const e=new Image;e.onload=()=>t(e.naturalWidth&&e.naturalHeight?e.naturalWidth/e.naturalHeight:0),e.onerror=()=>t(0),e.src=s}),o=this.host._spaceDialog;return o&&o.planUrl===t&&Number.isFinite(e)&&e>0?(this.host._spaceDialog={...o,savedAspect:e},e):0}if(await new Promise(t=>setTimeout(t,150)),this.host._spaceDialog?.planUrl!==t)return 0}return 0}async _deleteServerPlan(t){if(confirm(this.host._t("confirm.delete_plan",{name:t})))try{await this.host.hass.callWS({type:"houseplan/plans/delete",name:t});const s=this.host._spaceDialog;s?.saved&&(this.host._spaceDialog={...s,saved:s.saved.filter(s=>s.name!==t)})}catch(t){this.host._showToast(this.host._t("toast.plan_delete_failed",{err:this.host._errText(t)}))}}_renderServerPlans(t){if(t.savedBusy)return a`<div class="savedplans muted">${this.host._t("space.loading")}</div>`;const s=t.saved||[];if(!s.length)return a`<div class="savedplans muted">${this.host._t("space.no_saved")}</div>`;return a`<div class="savedplans">
|
||||
${s.map(s=>{return a`
|
||||
<div class="savedplan ${s.url===t.planUrl?"cur":""}">
|
||||
@@ -1 +1 @@
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="fcf6976c5dddbb163f794a984a8e4b93955f5a1fe83373c94dd2abb9e5162e56";try{await import("./houseplan-assets/houseplan-card-Dhz45m9k.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="aa9514085162d317577432073166a2385a0d511669f776f5268540be9317efef";try{await import("./houseplan-assets/houseplan-card-CaTFHDy3.js")}catch(e){if(!customElements.get("houseplan-card")){const l=String(navigator.language||"en").toLowerCase();const m=l.startsWith("ru")?"House Plan обновился — перезагрузите страницу (Ctrl+F5).":l.startsWith("de")?"House Plan wurde aktualisiert — bitte laden Sie die Seite neu (Strg+F5).":l.startsWith("fr")?"House Plan a été mis à jour — veuillez recharger la page (Ctrl+F5).":"House Plan was updated — please reload the page (Ctrl+F5).";customElements.define("houseplan-card",class extends HTMLElement{setConfig(){}getCardSize(){return 1}connectedCallback(){this.style.cssText="display:block;box-sizing:border-box;padding:16px;border:1px solid var(--divider-color,#e0e0e0);border-radius:var(--ha-card-border-radius,12px);background:var(--card-background-color,#fff);color:var(--primary-text-color,#212121);font:14px/1.4 var(--paper-font-body1_-_font-family,sans-serif)";this.textContent=m}})}console.error("[houseplan] stale entry: the main chunk is unavailable",e)}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The Device editor now has its own Undo/Redo history for marker positions.
|
||||
One completed drag is one step; cancelled or failed moves add nothing, and
|
||||
the 50-step session history is available from persistent toolbar buttons or
|
||||
`Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` and `Ctrl+Y`
|
||||
([#74](https://github.com/Matysh/houseplan-card/issues/74)).
|
||||
|
||||
- Device discovery lost its last hidden knobs: light grouping and the
|
||||
excluded-integrations list are now visible **Discovery filters** on the
|
||||
catalog's Available tab, with appear/disappear counters before saving and
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- В редакторе устройств появилась отдельная история отмены и повтора позиций
|
||||
маркеров. Одно завершённое перетаскивание — один шаг; отменённые и неудачные
|
||||
перемещения в историю не попадают. До 50 шагов текущей сессии доступны через
|
||||
постоянные кнопки панели или `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` и `Ctrl+Y`
|
||||
([#74](https://github.com/Matysh/houseplan-card/issues/74)).
|
||||
|
||||
- У обнаружения устройств не осталось скрытых ручек: группировка света и
|
||||
список исключённых интеграций стали видимыми «Фильтрами обнаружения» на
|
||||
вкладке «Доступны» каталога — со счётчиками «появится/скроется» до записи
|
||||
|
||||
@@ -1735,8 +1735,20 @@ separately promised workflows:
|
||||
icon preview; opening/saving untouched must keep the explicit override
|
||||
empty, and the hint disappears once an explicit icon is picked
|
||||
[auto: smoke_icon_placeholder]
|
||||
- [ ] No Reset button (v1.33.2): the Device editor toolbar has three tools —
|
||||
add, show all, icon rules; the layout-wiping Reset is gone [auto: smoke_editor_tabs]
|
||||
- [ ] Device-position history (#74): the Device editor always shows Undo/Redo
|
||||
before Close. One pointerup after a real snapped move creates exactly one
|
||||
point-wise server write and one command; Undo/Redo use update or delete
|
||||
as appropriate and preserve `s`, `k: 0` and unknown sibling fields.
|
||||
Pointercancel, lost capture, a second pointer, Escape, no movement and a
|
||||
failed write restore the preview and create no command. A failed history
|
||||
write restores the previous stack direction; own storage echoes preserve
|
||||
history while a different remote baseline, rebinding, deletion or
|
||||
optimization clears it. The independent stack is limited to 50 moves;
|
||||
`Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` and `Ctrl+Y` work outside native fields
|
||||
[unit: device-position-history; auto: smoke_device_position_history].
|
||||
- [ ] No Reset button (v1.33.2): the Device editor toolbar has three entry
|
||||
tools — add, show all, icon rules — plus position Undo/Redo and Close;
|
||||
the layout-wiping Reset is gone [auto: smoke_editor_tabs]
|
||||
- [ ] Grid in all editors + decor fade (v1.33.1): the dot grid shows in the
|
||||
Device and Background editors too (instant "I'm editing" cue), not in
|
||||
View; in the Background editor rooms/devices/openings/labels plus solid,
|
||||
|
||||
@@ -520,7 +520,13 @@ and its other entities remain deleted. The complete device stays available in
|
||||
|
||||
### Device editor
|
||||
|
||||
- drag a marker to save its server-side position;
|
||||
- drag a marker to save its server-side position. One completed drag creates
|
||||
one position-history step; a cancelled, unchanged or failed drag creates
|
||||
none;
|
||||
- persistent Undo/Redo buttons affect marker positions only. The same
|
||||
session-local history is available through `Ctrl/Cmd+Z`,
|
||||
`Ctrl/Cmd+Shift+Z` and `Ctrl+Y`, keeps up to 50 completed moves, and is not
|
||||
restored after reopening the card;
|
||||
- click it to edit name, binding, room, tap action and presentation;
|
||||
- **Add** opens the new-device dialog directly, without going through the
|
||||
device catalog;
|
||||
|
||||
@@ -791,7 +791,8 @@ binding tombstone не вырезает её из живого устройст
|
||||
|
||||
| Действие | Результат |
|
||||
|---|---|
|
||||
| Перетащить маркер | Меняет его общую серверную позицию; центр всегда оказывается на узле сетки |
|
||||
| Перетащить маркер | Меняет его общую серверную позицию; центр всегда оказывается на узле сетки. Одно завершённое перетаскивание создаёт один шаг истории, отменённое, неизменившее позицию или неудачное — ни одного |
|
||||
| Отменить / Повторить | Постоянные кнопки панели меняют только позиции маркеров. Та же отдельная история доступна по `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` и `Ctrl+Y`, хранит до 50 завершённых перемещений текущей сессии и не восстанавливается после повторного открытия карточки |
|
||||
| Клик по маркеру | Открывает диалог настройки |
|
||||
| **Добавить** | Сразу открывает диалог нового устройства, без перехода в каталог |
|
||||
| **Устройства** | Открывает единый каталог с поиском и вкладками «На плане», «Доступны», «Скрытые» и «Доступны снова»; у каждой привязки показаны причина, статус HA и допустимое следующее действие |
|
||||
|
||||
@@ -161,8 +161,13 @@ layer you cannot see is a layer you cannot edit.
|
||||
(binding, name, icon, size/angle, display as icon / icon + activity / value,
|
||||
activity color and size, tap override,
|
||||
model/link/description/PDFs, room).
|
||||
- The toolbar always keeps position-only Undo/Redo beside Close. One released
|
||||
drag is one command; cancellation, no movement and a failed write create no
|
||||
command. The independent 50-step session stack also uses `Ctrl/Cmd+Z`,
|
||||
`Ctrl/Cmd+Shift+Z` and `Ctrl+Y` and never absorbs marker configuration,
|
||||
lifecycle or Plan/Background edits.
|
||||
- + add device/entity/virtual, the bottom-left "Hide" / "Show" action in an
|
||||
existing device dialog (the one hiding mechanism, docs/FILTERING.md), ↺ reset layout,
|
||||
existing device dialog (the one hiding mechanism, docs/FILTERING.md),
|
||||
👁 "Show hidden" (local editor tool; replaced the shared show-all toggle),
|
||||
⬡ icon rules.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 105 KiB |
@@ -2,8 +2,8 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"oxipng": "oxipng 10.2.0",
|
||||
"sourceFingerprint": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"oxipng": "oxipng 10.1.1",
|
||||
"sourceFingerprint": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"captureScriptSha256": "fe76c8e2e27501e9a377d7aa15dd6cba6a018d55c07deb602dbee097f0a0a5ba",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -15,8 +15,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "f8c9541c9d7fde462d0efba63dff70944dc03df76ce988eecaf2b269e2863a96"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "29dad0fc79acf981b261b9bdc453915205044504a170e36bdedbffe239b09f54"
|
||||
},
|
||||
"view-touch": {
|
||||
"file": "02-view-touch.png",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "21c6fe7d79ff5c878ade8e98a64cc90dfd7bcc66338c864fb5a0bd411471313d"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "aed210e9627e3c49d6895a03cc444c553cb789a229767ceab7ded19d7bddb15c"
|
||||
},
|
||||
"space-create": {
|
||||
"file": "03-space-create.png",
|
||||
@@ -37,8 +37,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "fb32ea4a785fedb0e99b0bca6dbcf19022b32ced763ed875a57eb453be550bf5"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "16ad36b2dff1eace11b2946436e7e33a79ed11215cdcb084a8955e74275f8625"
|
||||
},
|
||||
"room-contour-close": {
|
||||
"file": "04-room-contour-close.png",
|
||||
@@ -48,8 +48,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "ca8bb143d0d6679ff09f8e9c1f5ad68b2ca7cfd96a4740a4a650c251f55dfba5"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "16b6e383d2fca408de5c00cde9272c63f34686785dc4ef3e9bb99b524c616101"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
"file": "05-plan-context-tray.png",
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "06b67c5108f9ee29a226cc2920484ccffe0c79840aec09721072b25b91ef8f03"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "9ab93efc87e8d5d22525e97383f43c0baf22cdb8c3f75b8a7fa1608b200d42e9"
|
||||
},
|
||||
"device-editor": {
|
||||
"file": "06-device-editor.png",
|
||||
@@ -70,8 +70,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "8be9c551b53bf072ee83b80963c9d9517348dc4d3856efe6ff0c1916e66f2c7e"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "16a8a765db13c9df12a04894926af805fd2166d1675b6a5c4ce51d30d9c06df7"
|
||||
},
|
||||
"device-display-preview": {
|
||||
"file": "06-device-display-preview.png",
|
||||
@@ -81,8 +81,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "cc5abe1b3364bf9812bea6ea5cfa84f35120ff9639ed3c4c92958cb646a8caf7"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "17ec126f49d87340b4712422180f3341bec9070df0706af5d9dd29bed4f56c76"
|
||||
},
|
||||
"background-editor": {
|
||||
"file": "07-background-editor.png",
|
||||
@@ -92,8 +92,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "d3b44c4f74c2f92d23e2346451f1a89a603ba6594e9ea32c9c1b53d0946fdad7"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "f258bbf604fc0b3e472a6c9bccc83874c2720f0e414a1a7b7439355a4a795ede"
|
||||
},
|
||||
"room-card": {
|
||||
"file": "08-room-card.png",
|
||||
@@ -103,8 +103,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "f1bc10b478f7afc68e70cb4c37148f680b0bbd41d50a7d63a21067fc952ff134"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "208d8baf186f1983951022d98e2ad30931ff4f03bc455ea23c36160f6fd1e4b9"
|
||||
},
|
||||
"device-info": {
|
||||
"file": "09-device-info.png",
|
||||
@@ -114,8 +114,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "06d233c045342dd141398dafca00d63123856cf36d84611513a22e81ba3843ba",
|
||||
"imageSha256": "2634cd7ead5720ce46ea4d6fa8f31006b740c520a73ddc97afe9a6d9d3b49778"
|
||||
"sourceSha256": "5cbe86c56ad056bab8b5d3efaee08859697b151843647800e6e4f8e70f556b76",
|
||||
"imageSha256": "43e70072d5bf2935547ded3cad7144a3fde88a856a80fcbcc86cf281c389a0cf"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function checkCoordinateWriteBarriers(root = defaultRoot) {
|
||||
}
|
||||
|
||||
const positionWrites = occurrences(frontend, "type: 'houseplan/layout/update'");
|
||||
if (positionWrites.length !== 2) errors.push(`frontend position writer inventory: ${positionWrites.length}`);
|
||||
if (positionWrites.length !== 3) errors.push(`frontend position writer inventory: ${positionWrites.length}`);
|
||||
for (const [ordinal, index] of positionWrites.entries()) requireWindow(
|
||||
errors, frontend, index,
|
||||
/const pos = canonicalizePosition\([^)]+\);[\s\S]*pos(?:[,\s}])/,
|
||||
|
||||
@@ -2282,6 +2282,17 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' const exact = false ? exactCoveringWall(walls, a, b, pitch, coordScale) : null;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'device-position-cancel-routed-to-commit',
|
||||
guard: 'node demo/smoke_device_position_history.mjs',
|
||||
because: 'pointer cancellation and lost capture must restore the uncommitted device preview '
|
||||
+ 'without creating a position command or writing layout (#74)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: '@pointercancel=${(e: PointerEvent) => this._pointerCancel(e, d)}',
|
||||
replace: '@pointercancel=${(e: PointerEvent) => this._pointerUp(e, d)}',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stale-space-position-guard-removed',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
@@ -2290,8 +2301,8 @@ const MUTANT_DEFINITIONS = [
|
||||
+ 'and websocket side effects when that space has been deleted or renamed',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (!this._spaceModelById(d.space)) return;\n if (this._norm) {',
|
||||
replace: ' if (this._norm) {',
|
||||
find: ' if (!this._spaceModelById(d.space)) return;\n this._layout = applyDevicePlacement(',
|
||||
replace: ' this._layout = applyDevicePlacement(',
|
||||
}],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
symbols: [
|
||||
'applyDevicePlacement', 'devicePlacement', 'sameDevicePlacement',
|
||||
'DevicePlacement', 'DevicePositionState',
|
||||
],
|
||||
smokes: ['smoke_device_position_history.mjs'],
|
||||
because: 'the #74 production-bundle scenario drives real Device editor pointer events and '
|
||||
+ 'observes transaction boundaries, point-wise update/delete, toolbar history, native field '
|
||||
+ 'shortcuts, cancellation and revision invalidation; none of those browser effects can be '
|
||||
+ 'proved by the pure placement helpers alone',
|
||||
},
|
||||
{
|
||||
symbols: [
|
||||
'resolveValueSource', 'valueSourceWriteFields', 'valueBadgeCandidates',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
export interface DevicePlacement {
|
||||
x: number;
|
||||
y: number;
|
||||
s?: string;
|
||||
}
|
||||
|
||||
export interface DeviceLayoutEntry extends Record<string, unknown> {
|
||||
x: number;
|
||||
y: number;
|
||||
s?: string;
|
||||
k?: number;
|
||||
}
|
||||
|
||||
export type DeviceLayout = Record<string, DeviceLayoutEntry>;
|
||||
|
||||
export interface DevicePositionState {
|
||||
deviceId: string;
|
||||
spaceId: string;
|
||||
placement: DevicePlacement | null;
|
||||
}
|
||||
|
||||
export function devicePlacement(layout: DeviceLayout, deviceId: string): DevicePlacement | null {
|
||||
const entry = layout[deviceId];
|
||||
if (!entry || !Number.isFinite(entry.x) || !Number.isFinite(entry.y)) return null;
|
||||
return {
|
||||
x: entry.x,
|
||||
y: entry.y,
|
||||
...(typeof entry.s === 'string' ? { s: entry.s } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function sameDevicePlacement(
|
||||
left: DevicePlacement | null,
|
||||
right: DevicePlacement | null,
|
||||
): boolean {
|
||||
if (left === null || right === null) return left === right;
|
||||
return left.x === right.x && left.y === right.y && left.s === right.s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply only placement fields. Existing/future sibling fields deliberately
|
||||
* survive an update; a null placement removes the complete persisted position
|
||||
* record because the layout schema does not permit a record without x/y.
|
||||
*/
|
||||
export function applyDevicePlacement(
|
||||
layout: DeviceLayout,
|
||||
deviceId: string,
|
||||
placement: DevicePlacement | null,
|
||||
): DeviceLayout {
|
||||
if (placement === null) {
|
||||
if (!(deviceId in layout)) return layout;
|
||||
const next = { ...layout };
|
||||
delete next[deviceId];
|
||||
return next;
|
||||
}
|
||||
|
||||
const current = { ...(layout[deviceId] || {}) } as Record<string, unknown>;
|
||||
delete current.x;
|
||||
delete current.y;
|
||||
delete current.s;
|
||||
const entry: DeviceLayoutEntry = {
|
||||
...current,
|
||||
x: placement.x,
|
||||
y: placement.y,
|
||||
...(placement.s !== undefined ? { s: placement.s } : {}),
|
||||
};
|
||||
return { ...layout, [deviceId]: entry };
|
||||
}
|
||||
@@ -241,6 +241,14 @@ import { hasTranslation, langOf, t, type I18nKey } from './i18n';
|
||||
import { LANGUAGE_RUNTIME, subscribeLanguageLoadFailures } from './i18n/registry';
|
||||
import { languageLoadingTemplate, languageRenderGate } from './i18n/language-runtime';
|
||||
import { CommandStack } from './command-stack';
|
||||
import {
|
||||
applyDevicePlacement,
|
||||
devicePlacement,
|
||||
sameDevicePlacement,
|
||||
type DeviceLayout,
|
||||
type DevicePlacement,
|
||||
type DevicePositionState,
|
||||
} from './device-position-history';
|
||||
import { resolvedSvgScreenBlend, svgScreenBlendSupported } from './glow-blend';
|
||||
import {
|
||||
buildGlowClipGeometry, buildLightBarrierScene, createGlowRuntimeState,
|
||||
@@ -789,6 +797,21 @@ interface DeviceInboxDialogState {
|
||||
|
||||
type FixedFloorState = FixedFloorSelection | { kind: 'pending'; value: unknown };
|
||||
|
||||
interface DeviceDragState {
|
||||
id: string;
|
||||
spaceId: string;
|
||||
displayName: string;
|
||||
pointerId: number;
|
||||
source: Element | null;
|
||||
sx: number;
|
||||
sy: number;
|
||||
ox: number;
|
||||
oy: number;
|
||||
moved: boolean;
|
||||
before: DevicePlacement | null;
|
||||
start: DevicePlacement;
|
||||
}
|
||||
|
||||
export class HouseplanCard extends LitElement {
|
||||
public _editorRuntime: import('./houseplan-editor-runtime').HouseplanEditorRuntime | null = null;
|
||||
public _onboardingRuntime: import('./houseplan-onboarding-runtime').HouseplanOnboardingRuntime | null = null;
|
||||
@@ -925,7 +948,7 @@ export class HouseplanCard extends LitElement {
|
||||
private _config?: CardConfig;
|
||||
|
||||
private _space = 'f1';
|
||||
private _layout: Record<string, { x: number; y: number; s?: string; k?: number }> = {};
|
||||
private _layout: DeviceLayout = {};
|
||||
private _serverStorage = false;
|
||||
private _loadOk = false;
|
||||
/** null until config/get answers; then mirrors auth.may_write for this user. */
|
||||
@@ -1702,6 +1725,9 @@ export class HouseplanCard extends LitElement {
|
||||
private _tool: MarkupTool = 'draw';
|
||||
/** UX-04: one named, 50-step command history for every plan-geometry tool. */
|
||||
private _geometryHistory = new CommandStack<SpaceGeometryState>(50);
|
||||
/** #74: independent session-local history for manual device placements. */
|
||||
private _devicePositionHistory = new CommandStack<DevicePositionState>(50);
|
||||
private _devicePositionBusy = false;
|
||||
/** Wall-thickness tool dialog (docs/WALL-THICKNESS.md). */
|
||||
private _wallDialog: {
|
||||
a: number[]; b: number[];
|
||||
@@ -2401,6 +2427,7 @@ export class HouseplanCard extends LitElement {
|
||||
};
|
||||
|
||||
private _drag: { id: string; sx: number; sy: number; ox: number; oy: number; moved: boolean } | null = null;
|
||||
private _deviceDrag: DeviceDragState | null = null;
|
||||
private _rlResize: { id: string; space: string; k0: number; cx: number; cy: number; d0: number } | null = null;
|
||||
private _holdTimer?: number;
|
||||
private _holdFired = false;
|
||||
@@ -2642,6 +2669,7 @@ export class HouseplanCard extends LitElement {
|
||||
this._bootTimer = undefined; // AUD-1552-01: a cleared id must not block the reconnect watcher
|
||||
clearTimeout(this._bootSoftTimer);
|
||||
this._saveConfigDebounced.flush(); // never leave an edit unsent on teardown
|
||||
this._cancelDeviceDrag();
|
||||
window.removeEventListener('hashchange', this._onHashChange);
|
||||
this._labsUnsub?.();
|
||||
this._labsUnsub = undefined;
|
||||
@@ -2813,6 +2841,24 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._mode === 'devices') {
|
||||
if ((undo || redo) && inField) return;
|
||||
if (redo) {
|
||||
e.preventDefault();
|
||||
this._redoDevicePosition();
|
||||
return;
|
||||
}
|
||||
if (undo) {
|
||||
e.preventDefault();
|
||||
this._undoDevicePosition();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape' && this._deviceDrag) {
|
||||
e.preventDefault();
|
||||
this._cancelDeviceDrag();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this._markup) return;
|
||||
if ((undo || redo) && inField) return; // keep native text-field history
|
||||
if ((e.key === 'Delete' || e.key === 'Backspace') && this._physicalSel
|
||||
@@ -3810,6 +3856,7 @@ export class HouseplanCard extends LitElement {
|
||||
this._pinchStart = null;
|
||||
this._swipeStart = null;
|
||||
this._drag = null;
|
||||
this._deviceDrag = null;
|
||||
this._rlResize = null;
|
||||
this._vacFit = null;
|
||||
this._compassDrag = false;
|
||||
@@ -3817,6 +3864,7 @@ export class HouseplanCard extends LitElement {
|
||||
this._mode = 'view';
|
||||
this._clearGeometryGesture();
|
||||
this._geometryHistory.clear();
|
||||
this._devicePositionHistory.clear();
|
||||
this._resumeDraftBySpace = {};
|
||||
this._tip = null;
|
||||
this._hoverRoom = null;
|
||||
@@ -4045,6 +4093,8 @@ export class HouseplanCard extends LitElement {
|
||||
// A genuinely different baseline invalidates local geometry undo. A
|
||||
// reconnect echo with identical content deliberately does not.
|
||||
this._geometryHistory.clear();
|
||||
this._devicePositionHistory.clear();
|
||||
this._cancelDeviceDrag();
|
||||
this._pendingPhysicalWrites.clear();
|
||||
if (this._serverCfg) this._clearGeometryGesture();
|
||||
this._serverCfg = nextConfig;
|
||||
@@ -4073,6 +4123,8 @@ export class HouseplanCard extends LitElement {
|
||||
layoutChanged = nextLayoutFingerprint !== (this._layoutContentFingerprint
|
||||
|| contentFingerprint(this._layout));
|
||||
if (layoutChanged) {
|
||||
this._devicePositionHistory.clear();
|
||||
this._cancelDeviceDrag();
|
||||
this._layout = nextLayout;
|
||||
this._layoutContentFingerprint = nextLayoutFingerprint;
|
||||
}
|
||||
@@ -4741,12 +4793,18 @@ export class HouseplanCard extends LitElement {
|
||||
const resp = await this.hass.callWS({ type: 'houseplan/layout/get' });
|
||||
const remote = resp?.layout || {};
|
||||
const merged: Record<string, any> = { ...remote };
|
||||
for (const [id, pos] of mine) merged[id] = pos;
|
||||
const fingerprint = contentFingerprint(merged);
|
||||
if (fingerprint !== (this._layoutContentFingerprint || contentFingerprint(this._layout))) {
|
||||
this._layout = merged;
|
||||
this._layoutContentFingerprint = fingerprint;
|
||||
for (const [id, pos] of mine) {
|
||||
if (pos === null) delete merged[id];
|
||||
else merged[id] = pos;
|
||||
}
|
||||
const fingerprint = contentFingerprint(merged);
|
||||
const differsFromCurrent = fingerprint !== contentFingerprint(this._layout);
|
||||
if (differsFromCurrent) {
|
||||
this._cancelDeviceDrag();
|
||||
this._devicePositionHistory.clear();
|
||||
this._layout = merged;
|
||||
}
|
||||
this._layoutContentFingerprint = fingerprint;
|
||||
this._layoutRev = resp?.rev ?? this._layoutRev;
|
||||
this._canOptimizeUndo = !!resp?.can_optimize_undo;
|
||||
this._haIntegrationVersion = typeof resp?.integration_version === 'string'
|
||||
@@ -4764,7 +4822,12 @@ export class HouseplanCard extends LitElement {
|
||||
|
||||
private _dirtyPos = new Set<string>();
|
||||
/** Positions sent to the server and not acknowledged yet (HP-1461-02). */
|
||||
private _sentPos = new Map<string, { s?: string; x: number; y: number }>();
|
||||
private _sentPos = new Map<string, DeviceLayout[string] | null>();
|
||||
|
||||
private _persistLocalLayout(): void {
|
||||
this._layout = canonicalizeLayoutGeometry(this._layout);
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(this._layout));
|
||||
}
|
||||
|
||||
private _persistLayout = debounce(() => {
|
||||
if (this._serverStorage) {
|
||||
@@ -4786,8 +4849,7 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
this._cacheSnapshot();
|
||||
} else {
|
||||
this._layout = canonicalizeLayoutGeometry(this._layout);
|
||||
localStorage.setItem(LS_KEY, JSON.stringify(this._layout));
|
||||
this._persistLocalLayout();
|
||||
}
|
||||
}, 600);
|
||||
|
||||
@@ -4850,8 +4912,10 @@ export class HouseplanCard extends LitElement {
|
||||
|| this._devices.find((d) => d.id === this._infoCard?.id)?.bindingStatus?.kind === 'ha_disabled') {
|
||||
this._closeInfoCard();
|
||||
}
|
||||
if (this._drag && this._devices.find((d) => d.id === this._drag!.id)?.bindingStatus?.kind === 'ha_disabled') {
|
||||
this._drag = null;
|
||||
if (this._deviceDrag
|
||||
&& this._devices.find((d) => d.id === this._deviceDrag!.id)?.bindingStatus?.kind === 'ha_disabled') {
|
||||
this._cancelDeviceDrag();
|
||||
this._devicePositionHistory.clear();
|
||||
}
|
||||
clearTimeout(this._holdTimer);
|
||||
this._holdFired = false;
|
||||
@@ -4998,24 +5062,135 @@ export class HouseplanCard extends LitElement {
|
||||
return snapPt(spaceCenter(s));
|
||||
}
|
||||
|
||||
private _devicePlacementForCanvas(d: DevItem, x: number, y: number): DevicePlacement {
|
||||
if (!this._norm) return { x: Math.round(x), y: Math.round(y) };
|
||||
const g = this._gridPitch;
|
||||
const gx = Math.round(x / g) * g;
|
||||
const gy = Math.round(y / g) * g;
|
||||
return {
|
||||
s: d.space,
|
||||
x: clampCanvasN(gx / NORM_W),
|
||||
y: clampCanvasN(gy / NORM_W),
|
||||
};
|
||||
}
|
||||
|
||||
private _previewDevicePlacement(deviceId: string, placement: DevicePlacement | null): void {
|
||||
this._layout = applyDevicePlacement(this._layout, deviceId, placement);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Abort an uncommitted device preview. Returns true when a drag existed. */
|
||||
private _cancelDeviceDrag(): boolean {
|
||||
const drag = this._deviceDrag;
|
||||
if (!drag) return false;
|
||||
this._deviceDrag = null;
|
||||
try {
|
||||
if (drag.source?.hasPointerCapture?.(drag.pointerId)) {
|
||||
drag.source.releasePointerCapture(drag.pointerId);
|
||||
}
|
||||
} catch {
|
||||
// Pointer capture may already have ended before the cancellation signal.
|
||||
}
|
||||
this._previewDevicePlacement(drag.id, drag.before);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** One serial, point-wise position write used by drag commit and history. */
|
||||
private async _persistDevicePlacement(
|
||||
deviceId: string,
|
||||
placement: DevicePlacement | null,
|
||||
): Promise<void> {
|
||||
this._layout = applyDevicePlacement(this._layout, deviceId, placement);
|
||||
if (this._serverStorage) {
|
||||
let pending: DeviceLayout[string] | null = null;
|
||||
let registered = false;
|
||||
try {
|
||||
let response: unknown;
|
||||
if (placement === null) {
|
||||
pending = null;
|
||||
this._sentPos.set(deviceId, pending);
|
||||
registered = true;
|
||||
response = await this.hass.callWS({
|
||||
type: 'houseplan/layout/delete', device_id: deviceId,
|
||||
});
|
||||
} else {
|
||||
const pos = canonicalizePosition(this._layout[deviceId]);
|
||||
pending = pos;
|
||||
this._sentPos.set(deviceId, pending);
|
||||
registered = true;
|
||||
response = await this.hass.callWS({
|
||||
type: 'houseplan/layout/update', device_id: deviceId, pos,
|
||||
});
|
||||
}
|
||||
this._noteLayoutRev(response);
|
||||
this._layoutContentFingerprint = contentFingerprint(this._layout);
|
||||
this._cacheSnapshot();
|
||||
} finally {
|
||||
if (registered && this._sentPos.get(deviceId) === pending) this._sentPos.delete(deviceId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._persistLocalLayout();
|
||||
}
|
||||
|
||||
private _devicePositionStateValid(state: DevicePositionState): boolean {
|
||||
const device = this._devices.find((candidate) => candidate.id === state.deviceId);
|
||||
return !!device
|
||||
&& device.space === state.spaceId
|
||||
&& device.bindingStatus?.kind !== 'ha_disabled'
|
||||
&& !!this._spaceModelById(state.spaceId);
|
||||
}
|
||||
|
||||
private async _runDevicePositionHistory(direction: 'undo' | 'redo'): Promise<void> {
|
||||
if (this._cancelDeviceDrag() || this._devicePositionBusy) return;
|
||||
const command = direction === 'undo'
|
||||
? this._devicePositionHistory.undo()
|
||||
: this._devicePositionHistory.redo();
|
||||
if (!command) return;
|
||||
const target = direction === 'undo' ? command.before : command.after;
|
||||
if (!this._devicePositionStateValid(target)) {
|
||||
this._devicePositionHistory.clear();
|
||||
this._showToast(this._t('history.device_stale'));
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
const rollback = devicePlacement(this._layout, target.deviceId);
|
||||
if (target.spaceId !== this._space) {
|
||||
this._commitSpace(target.spaceId);
|
||||
this._restoreZoom();
|
||||
}
|
||||
this._devicePositionBusy = true;
|
||||
this._previewDevicePlacement(target.deviceId, target.placement);
|
||||
try {
|
||||
await this._persistDevicePlacement(target.deviceId, target.placement);
|
||||
this._showToast(this._t(direction === 'undo' ? 'history.undone' : 'history.redone', {
|
||||
name: command.name,
|
||||
}));
|
||||
} catch (error: unknown) {
|
||||
this._previewDevicePlacement(target.deviceId, rollback);
|
||||
if (direction === 'undo') this._devicePositionHistory.redo();
|
||||
else this._devicePositionHistory.undo();
|
||||
this._showToast(this._t('toast.pos_save_failed', { err: this._errText(error) }));
|
||||
} finally {
|
||||
this._devicePositionBusy = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private _undoDevicePosition(): void {
|
||||
void this._runDevicePositionHistory('undo');
|
||||
}
|
||||
|
||||
private _redoDevicePosition(): void {
|
||||
void this._runDevicePositionHistory('redo');
|
||||
}
|
||||
|
||||
private _savePos(d: DevItem, x: number, y: number): void {
|
||||
if (!this._spaceModelById(d.space)) return;
|
||||
if (this._norm) {
|
||||
// The icon center snaps to the nodes of the same grid as the room markup
|
||||
// (docs/CANVAS.md §9). UX-05 has no free-position escape hatch.
|
||||
const g = this._gridPitch;
|
||||
const gx = Math.round(x / g) * g;
|
||||
const gy = Math.round(y / g) * g;
|
||||
|
||||
const prevK = (this._layout[d.id] as any)?.k;
|
||||
this._layout = {
|
||||
...this._layout,
|
||||
[d.id]: { s: d.space, x: clampCanvasN(gx / NORM_W), y: clampCanvasN(gy / NORM_W),
|
||||
...(prevK ? { k: prevK } : {}) },
|
||||
};
|
||||
} else {
|
||||
this._layout = { ...this._layout, [d.id]: { x: Math.round(x), y: Math.round(y) } };
|
||||
}
|
||||
this._layout = applyDevicePlacement(
|
||||
this._layout, d.id, this._devicePlacementForCanvas(d, x, y),
|
||||
);
|
||||
this._dirtyPos.add(d.id);
|
||||
this._persistLayout();
|
||||
}
|
||||
@@ -5191,7 +5366,7 @@ export class HouseplanCard extends LitElement {
|
||||
// if a future CSS change accidentally makes a marker a hit target again.
|
||||
if (this._mode !== 'view' && this._mode !== 'devices') return;
|
||||
ev.stopPropagation();
|
||||
if (this._drag?.moved || this._suppressClick || this._holdFired) return;
|
||||
if (this._deviceDrag?.moved || this._suppressClick || this._holdFired) return;
|
||||
if (this._mode === 'devices') {
|
||||
this._openMarkerDialog(d);
|
||||
return;
|
||||
@@ -6103,7 +6278,7 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
}
|
||||
// do not interfere with icon and label dragging
|
||||
if (this._drag) return;
|
||||
if (this._drag || this._deviceDrag) return;
|
||||
if (this._markup) {
|
||||
// Drawing is CLICK-based, so gestures coexist with it: a finger that
|
||||
// MOVES pans, two fingers pinch, and _suppressClick keeps the release
|
||||
@@ -6370,26 +6545,44 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
// A disabled ghost is a service entry point, not a movable plan object.
|
||||
// Leave the click intact so the settings dialog still opens.
|
||||
if (d.bindingStatus?.kind === 'ha_disabled') return;
|
||||
if (d.bindingStatus?.kind === 'ha_disabled' || this._devicePositionBusy) return;
|
||||
if (this._deviceDrag) {
|
||||
if (this._deviceDrag.pointerId !== ev.pointerId) this._cancelDeviceDrag();
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
const p = this._pos(d);
|
||||
this._drag = { id: d.id, sx: ev.clientX, sy: ev.clientY, ox: p.x, oy: p.y, moved: false };
|
||||
this._deviceDrag = {
|
||||
id: d.id,
|
||||
spaceId: d.space,
|
||||
displayName: d.name,
|
||||
pointerId: ev.pointerId,
|
||||
source: ev.currentTarget as Element | null,
|
||||
sx: ev.clientX,
|
||||
sy: ev.clientY,
|
||||
ox: p.x,
|
||||
oy: p.y,
|
||||
moved: false,
|
||||
before: devicePlacement(this._layout, d.id),
|
||||
start: this._devicePlacementForCanvas(d, p.x, p.y),
|
||||
};
|
||||
capturePointer(ev);
|
||||
this._tip = null;
|
||||
}
|
||||
|
||||
private _pointerMove(ev: PointerEvent, d: DevItem): void {
|
||||
if (this._mode !== 'devices') return;
|
||||
if (!this._drag || this._drag.id !== d.id) return;
|
||||
const drag = this._deviceDrag;
|
||||
if (!drag || drag.id !== d.id || drag.pointerId !== ev.pointerId) return;
|
||||
const stage = this.renderRoot.querySelector('.stage') as HTMLElement;
|
||||
if (!stage) return;
|
||||
const vb = this._baseVb();
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const v = this._viewOr(vb);
|
||||
const dx = ((ev.clientX - this._drag.sx) / rect.width) * v.w;
|
||||
const dy = ((ev.clientY - this._drag.sy) / rect.height) * v.h;
|
||||
if (Math.abs(ev.clientX - this._drag.sx) + Math.abs(ev.clientY - this._drag.sy) > 3) {
|
||||
this._drag.moved = true;
|
||||
const dx = ((ev.clientX - drag.sx) / rect.width) * v.w;
|
||||
const dy = ((ev.clientY - drag.sy) / rect.height) * v.h;
|
||||
if (Math.abs(ev.clientX - drag.sx) + Math.abs(ev.clientY - drag.sy) > 3) {
|
||||
drag.moved = true;
|
||||
clearTimeout(this._holdTimer);
|
||||
}
|
||||
// DEV-B58-01. This used to be clamped into `vb` — the CONTENT FRAME — with
|
||||
@@ -6399,21 +6592,54 @@ export class HouseplanCard extends LitElement {
|
||||
// where the new room was going to be. The plan has no edges any more
|
||||
// (docs/CANVAS.md §9); the only bound is the garbage limit the backend
|
||||
// enforces, and it is the SAME ±5000 on both sides of the wire.
|
||||
const nx = clampCanvasR(this._drag.ox + dx);
|
||||
const ny = clampCanvasR(this._drag.oy + dy);
|
||||
this._savePos(d, nx, ny);
|
||||
const nx = clampCanvasR(drag.ox + dx);
|
||||
const ny = clampCanvasR(drag.oy + dy);
|
||||
this._previewDevicePlacement(d.id, this._devicePlacementForCanvas(d, nx, ny));
|
||||
}
|
||||
|
||||
private _pointerUp(_ev: PointerEvent, d: DevItem): void {
|
||||
private _pointerUp(ev: PointerEvent, d: DevItem): void {
|
||||
clearTimeout(this._holdTimer);
|
||||
if (this._mode !== 'devices') return;
|
||||
if (!this._drag || this._drag.id !== d.id) return;
|
||||
const moved = this._drag.moved;
|
||||
this._drag = moved ? this._drag : null;
|
||||
if (moved) {
|
||||
this._selId = d.id;
|
||||
window.setTimeout(() => (this._drag = null), 0);
|
||||
const drag = this._deviceDrag;
|
||||
if (!drag || drag.id !== d.id || drag.pointerId !== ev.pointerId) return;
|
||||
this._deviceDrag = null;
|
||||
const after = devicePlacement(this._layout, d.id);
|
||||
if (!drag.moved || after === null || sameDevicePlacement(after, drag.start)) {
|
||||
this._previewDevicePlacement(d.id, drag.before);
|
||||
return;
|
||||
}
|
||||
|
||||
this._selId = d.id;
|
||||
this._suppressClick = true;
|
||||
window.setTimeout(() => { this._suppressClick = false; }, 0);
|
||||
this._devicePositionBusy = true;
|
||||
this.requestUpdate();
|
||||
void this._persistDevicePlacement(d.id, after)
|
||||
.then(() => {
|
||||
const name = this._t('history.device_move', { name: drag.displayName });
|
||||
this._devicePositionHistory.push({
|
||||
name,
|
||||
before: {
|
||||
deviceId: drag.id, spaceId: drag.spaceId, placement: drag.before,
|
||||
},
|
||||
after: {
|
||||
deviceId: drag.id, spaceId: drag.spaceId, placement: after,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
this._previewDevicePlacement(d.id, drag.before);
|
||||
this._showToast(this._t('toast.pos_save_failed', { err: this._errText(error) }));
|
||||
})
|
||||
.finally(() => {
|
||||
this._devicePositionBusy = false;
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private _pointerCancel(ev: PointerEvent, d: DevItem): void {
|
||||
if (this._deviceDrag?.id !== d.id || this._deviceDrag.pointerId !== ev.pointerId) return;
|
||||
this._cancelDeviceDrag();
|
||||
}
|
||||
|
||||
private _showToast(msg: string): void {
|
||||
@@ -6576,7 +6802,7 @@ export class HouseplanCard extends LitElement {
|
||||
): void {
|
||||
this._notePointer(ev);
|
||||
if (!this._pointerModality.hoverEnabled) return;
|
||||
if (this._drag) return;
|
||||
if (this._drag || this._deviceDrag) return;
|
||||
this._tip = { x: ev.clientX, y: ev.clientY, title, meta, lqi, temp, hum };
|
||||
}
|
||||
|
||||
@@ -11804,7 +12030,8 @@ export class HouseplanCard extends LitElement {
|
||||
this._showTip(e, d.name, presentation.haDisabled ? ghostLabel : metrics);
|
||||
}}
|
||||
@pointerup=${(e: PointerEvent) => this._pointerUp(e, d)}
|
||||
@pointercancel=${(e: PointerEvent) => this._pointerUp(e, d)}
|
||||
@pointercancel=${(e: PointerEvent) => this._pointerCancel(e, d)}
|
||||
@lostpointercapture=${(e: PointerEvent) => this._pointerCancel(e, d)}
|
||||
>
|
||||
${renderDeviceFace(presentation, {
|
||||
surface: 'interactive-plan',
|
||||
@@ -12127,8 +12354,8 @@ export class HouseplanCard extends LitElement {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (this._mode === 'devices' && this._drag?.moved) {
|
||||
const d = this._devices.find((x) => x.id === this._drag!.id);
|
||||
if (this._mode === 'devices' && this._deviceDrag?.moved) {
|
||||
const d = this._devices.find((x) => x.id === this._deviceDrag!.id);
|
||||
return d ? (() => { const p = this._pos(d); return [p.x, p.y]; })() : null;
|
||||
}
|
||||
if (this._mode === 'decor') {
|
||||
|
||||
@@ -245,6 +245,7 @@ import { enqueueSerializedWrite } from './serialized-write-queue';
|
||||
import { hasTranslation, langOf, t, type I18nKey } from './i18n';
|
||||
import { classifyPlanFile, encodePlanFile, renderBackdropGuard } from './backdrop-pick';
|
||||
import { CommandStack } from './command-stack';
|
||||
import type { DeviceLayout, DevicePositionState } from './device-position-history';
|
||||
import { resolvedSvgScreenBlend, svgScreenBlendSupported } from './glow-blend';
|
||||
import {
|
||||
CONTINUITY_LONG_HIDDEN_MS,
|
||||
@@ -854,6 +855,7 @@ export interface HouseplanEditorHostPort {
|
||||
_canEdit: boolean;
|
||||
_canOptimizeUndo: boolean;
|
||||
_cancelDevicePressFeedback: () => void;
|
||||
_cancelDeviceDrag: () => boolean;
|
||||
_cancelModeTransition: (commitTarget?: boolean) => void;
|
||||
_candidateDeviceSnapshot: RenderDeviceSnapshot | null;
|
||||
_capturedSnapshotConfigEpoch: number;
|
||||
@@ -898,6 +900,8 @@ export interface HouseplanEditorHostPort {
|
||||
_deviceInboxMemo: { key: string; rows: DeviceInboxRow[]; } | null;
|
||||
_deviceInboxReturn: DeviceInboxDialogState | null;
|
||||
_devicePresentation: (d: DevItem, showLqi?: boolean, designPreview?: boolean) => ResolvedDevicePresentation;
|
||||
_devicePositionBusy: boolean;
|
||||
_devicePositionHistory: CommandStack<DevicePositionState>;
|
||||
_devices: DevItem[];
|
||||
_dirtyPos: Set<string>;
|
||||
_display: (url: string | null | undefined) => string;
|
||||
@@ -963,7 +967,7 @@ export interface HouseplanEditorHostPort {
|
||||
_kioskScale: { icon: number; font: number; };
|
||||
_labsIso: boolean;
|
||||
_lastValidStageSize: [number, number] | null;
|
||||
_layout: Record<string, { x: number; y: number; s?: string; k?: number; }>;
|
||||
_layout: DeviceLayout;
|
||||
_layoutRev: number;
|
||||
_logicalViewCenter: (projection: "flat" | "iso") => { x: number; y: number; } | null;
|
||||
_markerDialog: { devId?: string; uploadId?: string; name: string; binding: string; bindingMode: "virtual" | "ha"; bindingOpen: boolean; showEntities: boolean; bindingFilter: string; icon: string; autoIcon: string; display: DeviceDisplayMode; rippleColor: string; rippleSize: number; size: number; angle: number; tapAction: string; tapActionTouched: boolean; originalHasTapAction: boolean; originalTapAction: string | null | undefined; tapHintAnnouncement: string; toggleEntity: string; toggleEntityTouched: boolean; originalHasToggleEntity: boolean; originalToggleEntity: string | null | undefined; tapTarget: string; tapConfirm: boolean; runFilter: string; controls: string[]; controlsFilter: string; glowRadius: string; lightRole: "auto" | "always" | "never"; lightRoleTouched: boolean; originalHasIsLight: boolean; originalIsLight: boolean | null | undefined; lightEntity: string; lightEntityTouched: boolean; originalHasLightEntity: boolean; originalLightEntity: string | null | undefined; glowMode: "auto" | "color" | "fixed"; glowColor: string; glowBrightness: number; glowColorDrafted: boolean; glowBrightnessDrafted: boolean; glowTouched: boolean; originalHasGlowColor: boolean; originalGlowColor: { c: string; bri?: number | null; } | null | undefined; valueBadgeEnabled: boolean; valueBadgeSource: ValueBadgeSource | null; valueBadgePosition: ValueBadgePosition; valueBadgeTouched: boolean; originalHasValueBadge: boolean; originalValueBadge: MarkerValueBadge | null | undefined; valueSource: ValueBadgeSource | null; valueSourceTouched: boolean; originalHasValueSource: boolean; originalValueSource: ValueBadgeSource | null | undefined; useClimateTemp: boolean; model: string; link: string; description: string; pdfs: PdfRef[]; room: string; hideFromPlan: boolean; busy: boolean; } | null;
|
||||
@@ -1052,6 +1056,7 @@ export interface HouseplanEditorHostPort {
|
||||
ResizePreview, ResizeLiveLabel[], SpaceGeometryState, ResizeWallUnion, ResizeWallArtifact
|
||||
>;
|
||||
_restoreZoom: () => void;
|
||||
_redoDevicePosition: () => void;
|
||||
_resumeDraftBySpace: Record<string, string>;
|
||||
_rlResize: { id: string; space: string; k0: number; cx: number; cy: number; d0: number; } | null;
|
||||
_roomCenter: (r: RoomCfg) => number[];
|
||||
@@ -1075,7 +1080,7 @@ export interface HouseplanEditorHostPort {
|
||||
_screenToVb: (sx: number, sy: number) => number[];
|
||||
_segments: number[][];
|
||||
_selId: string | null;
|
||||
_sentPos: Map<string, { s?: string; x: number; y: number; }>;
|
||||
_sentPos: Map<string, DeviceLayout[string] | null>;
|
||||
_serverCfg: ServerConfig | null;
|
||||
_serverStorage: boolean;
|
||||
_settings: { exclude_integrations?: string[]; group_lights?: boolean; show_all?: boolean; filter_seeded?: boolean; icon_rules?: { pattern: string; icon: string; }[]; };
|
||||
@@ -1097,6 +1102,7 @@ export interface HouseplanEditorHostPort {
|
||||
_suppressClick: boolean;
|
||||
_swipeStart: { x: number; y: number; id: number; } | null;
|
||||
_t: (key: I18nKey, vars?: Record<string, string | number>) => string;
|
||||
_undoDevicePosition: () => void;
|
||||
_thickWallCuts: () => number[][];
|
||||
_tip: { x: number; y: number; title: string; meta: string; lqi?: number | null; temp?: number | null; hum?: number | null; } | null;
|
||||
_toggleConfirmationLines: (intent: ResolvedToggleIntent) => string[];
|
||||
@@ -1212,6 +1218,7 @@ public _setMode(mode: 'view' | 'plan' | 'devices' | 'decor', animate = true): vo
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.host._mode === 'devices') this.host._cancelDeviceDrag();
|
||||
this.host._bootSoftCancel(); // navigation owns its own short, bounded transition
|
||||
if ((mode === 'plan' || mode === 'decor') && !this.host._norm) {
|
||||
this.host._showToast(this.host._t('toast.markup_needs_server'));
|
||||
@@ -8315,6 +8322,8 @@ public async _saveMarker(): Promise<void> {
|
||||
.catch(() => undefined); // leftovers are harmless; broken links are not
|
||||
}
|
||||
this._closeMarkerDialog();
|
||||
this.host._cancelDeviceDrag();
|
||||
this.host._devicePositionHistory.clear();
|
||||
this.host._regSignature = '';
|
||||
this.host._maybeRebuildDevices();
|
||||
this.host._showToast(this.host._t('toast.marker_saved'));
|
||||
@@ -8377,6 +8386,8 @@ public async _deleteMarker(): Promise<void> {
|
||||
this.host._deviceInboxReturn = { ...this.host._deviceInboxReturn, tab: 'readd', anchor: binding };
|
||||
}
|
||||
this._closeMarkerDialog();
|
||||
this.host._cancelDeviceDrag();
|
||||
this.host._devicePositionHistory.clear();
|
||||
if (this.host._infoCard?.id === targetId) this.host._closeInfoCard();
|
||||
if (this.host._selId === targetId) this.host._selId = null;
|
||||
if (this.host._drag?.id === targetId) this.host._drag = null;
|
||||
@@ -9127,6 +9138,8 @@ public async _runAlignToGrid(): Promise<void> {
|
||||
this.host._serverCfg = d.config;
|
||||
this.host._layout = d.layout;
|
||||
this.host._geometryHistory.clear();
|
||||
this.host._cancelDeviceDrag();
|
||||
this.host._devicePositionHistory.clear();
|
||||
this.host._cfgRev = resp?.config_rev ?? this.host._cfgRev + 1;
|
||||
this.host._layoutRev = resp?.layout_rev ?? this.host._layoutRev + 1;
|
||||
this.host._canOptimizeUndo = !!resp?.can_undo;
|
||||
@@ -9187,6 +9200,8 @@ public async _undoPlanOptimization(): Promise<void> {
|
||||
this.host._cfgRev = cfgResp?.rev ?? this.host._cfgRev;
|
||||
this.host._layout = layResp?.layout || this.host._layout;
|
||||
this.host._geometryHistory.clear();
|
||||
this.host._cancelDeviceDrag();
|
||||
this.host._devicePositionHistory.clear();
|
||||
this.host._layoutRev = layResp?.rev ?? this.host._layoutRev;
|
||||
this.host._canOptimizeUndo = false;
|
||||
this.host._undoKind = null;
|
||||
@@ -11741,6 +11756,14 @@ public _renderMarkupBar(): TemplateResult {
|
||||
}
|
||||
|
||||
public _renderDevicesBar(): TemplateResult {
|
||||
const undoName = this.host._devicePositionHistory.undoName;
|
||||
const redoName = this.host._devicePositionHistory.redoName;
|
||||
const undoTitle = undoName
|
||||
? this.host._t('history.undo_named', { name: undoName })
|
||||
: this.host._t('history.undo_empty');
|
||||
const redoTitle = redoName
|
||||
? this.host._t('history.redo_named', { name: redoName })
|
||||
: this.host._t('history.redo_empty');
|
||||
return html`<div class="editbar devbar">
|
||||
<div class="editbar-tools" tabindex="-1" ?inert=${this.host._modeTransitionBusy}>
|
||||
<ha-icon icon="mdi:tune-variant" class="warn"></ha-icon>
|
||||
@@ -11758,6 +11781,18 @@ public _renderDevicesBar(): TemplateResult {
|
||||
${this.host._editorToolbarGroups.map((group) => this._renderEditorGroupLauncher(group))}
|
||||
</div>
|
||||
<div class="editbar-end">
|
||||
<button class="btn ghost" data-device-position-history="undo"
|
||||
@click=${() => this.host._undoDevicePosition()}
|
||||
?disabled=${this.host._devicePositionBusy || !undoName}
|
||||
title=${undoTitle} aria-label=${undoTitle}>
|
||||
<ha-icon icon="mdi:undo-variant" aria-hidden="true"></ha-icon>
|
||||
</button>
|
||||
<button class="btn ghost" data-device-position-history="redo"
|
||||
@click=${() => this.host._redoDevicePosition()}
|
||||
?disabled=${this.host._devicePositionBusy || !redoName}
|
||||
title=${redoTitle} aria-label=${redoTitle}>
|
||||
<ha-icon icon="mdi:redo-variant" aria-hidden="true"></ha-icon>
|
||||
</button>
|
||||
<button class="btn barclose" title=${this.host._t('title.close_editor')}
|
||||
data-editor-navigation="view"
|
||||
@click=${() => this._setMode('view')}>
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"history.decor_transform": "Dekorobjekt transformieren",
|
||||
"history.decor_delete": "Dekorobjekt löschen",
|
||||
"history.backdrop_transform": "Plan-Hintergrund transformieren",
|
||||
"history.device_move": "{name} verschieben",
|
||||
"history.device_stale": "Der Positionsverlauf ist nicht mehr anwendbar und wurde gelöscht",
|
||||
"opening.new": "Neue Öffnung",
|
||||
"opening.edit": "Öffnung",
|
||||
"opening.door": "Tür",
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"history.decor_transform": "Transform decor object",
|
||||
"history.decor_delete": "Delete decor object",
|
||||
"history.backdrop_transform": "Transform plan backdrop",
|
||||
"history.device_move": "Move {name}",
|
||||
"history.device_stale": "Device position history is no longer applicable and was cleared",
|
||||
"opening.new": "New opening",
|
||||
"opening.edit": "Opening",
|
||||
"opening.door": "Door",
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"history.decor_transform": "Transformer l’objet décoratif",
|
||||
"history.decor_delete": "Supprimer l’objet décoratif",
|
||||
"history.backdrop_transform": "Transformer l’arrière-plan du plan",
|
||||
"history.device_move": "Déplacer {name}",
|
||||
"history.device_stale": "L’historique des positions n’est plus applicable et a été effacé",
|
||||
"opening.new": "Nouvelle ouverture",
|
||||
"opening.edit": "Ouverture",
|
||||
"opening.door": "Porte",
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
"history.decor_transform": "Трансформация объекта декора",
|
||||
"history.decor_delete": "Удаление объекта декора",
|
||||
"history.backdrop_transform": "Трансформация подложки плана",
|
||||
"history.device_move": "Перемещение: {name}",
|
||||
"history.device_stale": "История позиций больше неприменима и была очищена",
|
||||
"opening.new": "Новый проём",
|
||||
"opening.edit": "Проём",
|
||||
"opening.door": "Дверь",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyDevicePlacement,
|
||||
devicePlacement,
|
||||
sameDevicePlacement,
|
||||
} from '../test-build/device-position-history.js';
|
||||
|
||||
test('updates only placement and preserves zero and future sibling fields', () => {
|
||||
const original = {
|
||||
device: { s: 'floor-1', x: 0.1, y: 0.2, k: 0, future: { pinned: true } },
|
||||
};
|
||||
const next = applyDevicePlacement(original, 'device', {
|
||||
s: 'floor-2', x: 0.75, y: 0.5,
|
||||
});
|
||||
|
||||
assert.deepEqual(next.device, {
|
||||
s: 'floor-2', x: 0.75, y: 0.5, k: 0, future: { pinned: true },
|
||||
});
|
||||
assert.deepEqual(original.device, {
|
||||
s: 'floor-1', x: 0.1, y: 0.2, k: 0, future: { pinned: true },
|
||||
});
|
||||
assert.notEqual(next, original);
|
||||
assert.notEqual(next.device, original.device);
|
||||
});
|
||||
|
||||
test('legacy placement removes a newer space field without touching siblings', () => {
|
||||
const next = applyDevicePlacement({
|
||||
device: { s: 'floor-1', x: 0.1, y: 0.2, k: 3 },
|
||||
}, 'device', { x: 120, y: 240 });
|
||||
assert.deepEqual(next.device, { x: 120, y: 240, k: 3 });
|
||||
assert.deepEqual(devicePlacement(next, 'device'), { x: 120, y: 240 });
|
||||
});
|
||||
|
||||
test('null restores auto-position by removing the complete persisted record', () => {
|
||||
const original = {
|
||||
device: { s: 'floor-1', x: 0.1, y: 0.2, k: 0, future: 'value' },
|
||||
other: { s: 'floor-1', x: 0.3, y: 0.4 },
|
||||
};
|
||||
const next = applyDevicePlacement(original, 'device', null);
|
||||
assert.equal('device' in next, false);
|
||||
assert.deepEqual(next.other, original.other);
|
||||
assert.equal(devicePlacement(next, 'device'), null);
|
||||
});
|
||||
|
||||
test('missing delete is an identity no-op and placement equality includes space', () => {
|
||||
const layout = { other: { x: 1, y: 2 } };
|
||||
assert.equal(applyDevicePlacement(layout, 'missing', null), layout);
|
||||
assert.equal(sameDevicePlacement(null, null), true);
|
||||
assert.equal(sameDevicePlacement(null, { x: 1, y: 2 }), false);
|
||||
assert.equal(sameDevicePlacement({ x: 1, y: 2 }, { x: 1, y: 2 }), true);
|
||||
assert.equal(sameDevicePlacement(
|
||||
{ s: 'a', x: 1, y: 2 }, { s: 'b', x: 1, y: 2 },
|
||||
), false);
|
||||
});
|
||||
@@ -27,7 +27,7 @@
|
||||
"src/signing.ts", "src/initial-load.ts", "src/space-model-selection.ts", "src/space-dialog.ts",
|
||||
"src/visual-continuity.ts", "src/mode-transition.ts", "src/editor-runtime-loader.ts", "src/pointer-modality.ts",
|
||||
"src/render-device-snapshot.ts",
|
||||
"src/command-stack.ts",
|
||||
"src/command-stack.ts", "src/device-position-history.ts",
|
||||
"src/coordinate-canonicalization.ts",
|
||||
"src/serialized-write-queue.ts",
|
||||
"src/align-grid.ts",
|
||||
|
||||