Reorg root (#1559)

* Extract element from entrypoint

* Reorg root

* Extract more

* Lint

* Extract connection

* Extract notification

* Lint

* Also split out more info dialog

* Consolidate dynamic element creation
This commit is contained in:
Paulus Schoutsen 2018-08-11 08:45:11 +02:00 committed by GitHub
parent 1a31855fc8
commit 1b2b62f04c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 604 additions and 494 deletions

View File

@ -1,23 +0,0 @@
// Allows registering dialogs and makes sure they are appended to the root element.
export default (root) => {
root.addEventListener('register-dialog', (regEv) => {
let loaded = null;
const {
dialogShowEvent,
dialogTag,
dialogImport,
} = regEv.detail;
root.addEventListener(dialogShowEvent, (showEv) => {
if (!loaded) {
loaded = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag);
root.shadowRoot.appendChild(dialogEl);
return dialogEl;
});
}
loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
});
});
};

View File

@ -1,439 +1,24 @@
/* eslint-disable import/first */
// Load polyfill first so HTML imports start resolving
/* eslint-disable import/first */
import '../resources/html-import/polyfill.js';
import '@polymer/app-route/app-location.js';
import '@polymer/app-route/app-route.js';
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import '@polymer/paper-styles/typography.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { setPassiveTouchGestures } from '@polymer/polymer/lib/utils/settings.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import LocalizeMixin from '../mixins/localize-mixin.js';
import {
ERR_INVALID_AUTH,
subscribeEntities,
subscribeConfig,
} from 'home-assistant-js-websocket';
import translationMetadata from '../../build-translations/translationMetadata.json';
import '../layouts/home-assistant-main.js';
import '../resources/ha-style.js';
import '../util/ha-pref-storage.js';
import { getActiveTranslation, getTranslation } from '../util/hass-translation.js';
import '../util/legacy-support';
import '../resources/roboto.js';
import hassCallApi from '../util/hass-call-api.js';
import makeDialogManager from '../dialogs/dialog-manager.js';
import registerServiceWorker from '../util/register-service-worker.js';
import computeStateName from '../common/entity/compute_state_name.js';
import applyThemesOnElement from '../common/dom/apply_themes_on_element.js';
// For MDI icons. Needs to be part of main bundle or else it won't hook
// properly into iron-meta, which is used to transfer iconsets to iron-icon.
import '../components/ha-iconset-svg.js';
import '../layouts/app/home-assistant.js';
/* polyfill for paper-dropdown */
import(/* webpackChunkName: "polyfill-web-animations-next" */ 'web-animations-js/web-animations-next-lite.min.js');
import(/* webpackChunkName: "login-form" */ '../layouts/login-form.js');
import(/* webpackChunkName: "notification-manager" */ '../managers/notification-manager.js');
setPassiveTouchGestures(true);
/* LastPass createElement workaround. See #428 */
document.createElement = Document.prototype.createElement;
class HomeAssistant extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<ha-pref-storage hass="[[hass]]" id="storage"></ha-pref-storage>
<notification-manager id="notifications" hass="[[hass]]"></notification-manager>
<app-location route="{{route}}"></app-location>
<app-route route="{{route}}" pattern="/:panel" data="{{routeData}}"></app-route>
<template is="dom-if" if="[[showMain]]" restamp="">
<home-assistant-main on-hass-more-info="handleMoreInfo" on-hass-dock-sidebar="handleDockSidebar" on-hass-notification="handleNotification" on-hass-logout="handleLogout" hass="[[hass]]" route="{{route}}"></home-assistant-main>
</template>
<template is="dom-if" if="[[!showMain]]" restamp="">
<login-form hass="[[hass]]" connection-promise="{{connectionPromise}}" show-loading="[[computeShowLoading(connectionPromise, hass)]]">
</login-form>
</template>
`;
}
static get properties() {
return {
connectionPromise: {
type: Object,
value: window.hassConnection || null,
observer: 'handleConnectionPromise',
},
connection: {
type: Object,
value: null,
observer: 'connectionChanged',
},
hass: {
type: Object,
value: null,
},
showMain: {
type: Boolean,
computed: 'computeShowMain(hass)',
},
route: Object,
routeData: Object,
panelUrl: {
type: String,
computed: 'computePanelUrl(routeData)',
observer: 'panelUrlChanged',
},
};
}
constructor() {
super();
makeDialogManager(this);
}
ready() {
super.ready();
this.addEventListener('settheme', e => this.setTheme(e));
this.addEventListener('hass-language-select', e => this.selectLanguage(e));
this.loadResources();
afterNextRender(null, registerServiceWorker);
}
computeShowMain(hass) {
return hass && hass.states && hass.config && hass.panels;
}
computeShowLoading(connectionPromise, hass) {
// Show loading when connecting or when connected but not all pieces loaded yet
return (connectionPromise != null
|| (hass && hass.connection && (!hass.states || !hass.config)));
}
async loadResources(fragment) {
const result = await getTranslation(fragment);
this._updateResources(result.language, result.data);
}
async loadBackendTranslations() {
if (!this.hass.language) return;
const language = this.hass.selectedLanguage || this.hass.language;
const { resources } = await this.hass.callWS({
type: 'frontend/get_translations',
language,
});
// If we've switched selected languages just ignore this response
if ((this.hass.selectedLanguage || this.hass.language) !== language) return;
this._updateResources(language, resources);
}
_updateResources(language, data) {
// Update the language in hass, and update the resources with the newly
// loaded resources. This merges the new data on top of the old data for
// this language, so that the full translation set can be loaded across
// multiple fragments.
this._updateHass({
language: language,
resources: {
[language]: Object.assign({}, this.hass
&& this.hass.resources && this.hass.resources[language], data),
},
});
}
connectionChanged(conn, oldConn) {
if (oldConn) {
this.unsubConnection();
this.unsubConnection = null;
}
if (!conn) {
this._updateHass({
connection: null,
connected: false,
states: null,
config: null,
themes: null,
dockedSidebar: false,
moreInfoEntityId: null,
callService: null,
callApi: null,
sendWS: null,
callWS: null,
user: null,
});
return;
}
var notifications = this.$.notifications;
this.hass = Object.assign({
connection: conn,
connected: true,
states: null,
config: null,
themes: null,
panels: null,
panelUrl: this.panelUrl,
language: getActiveTranslation(),
// If resources are already loaded, don't discard them
resources: (this.hass && this.hass.resources) || null,
translationMetadata: translationMetadata,
dockedSidebar: false,
moreInfoEntityId: null,
callService: async (domain, service, serviceData) => {
try {
await conn.callService(domain, service, serviceData || {});
let message;
let name;
if (serviceData.entity_id && this.hass.states &&
this.hass.states[serviceData.entity_id]) {
name = computeStateName(this.hass.states[serviceData.entity_id]);
}
if (service === 'turn_on' && serviceData.entity_id) {
message = this.localize(
'ui.notification_toast.entity_turned_on',
'entity', name || serviceData.entity_id
);
} else if (service === 'turn_off' && serviceData.entity_id) {
message = this.localize(
'ui.notification_toast.entity_turned_off',
'entity', name || serviceData.entity_id
);
} else {
message = this.localize(
'ui.notification_toast.service_called',
'service', `${domain}/${service}`
);
}
notifications.showNotification(message);
} catch (err) {
const msg = this.localize(
'ui.notification_toast.service_call_failed',
'service', `${domain}/${service}`
);
notifications.showNotification(msg);
throw err;
}
},
callApi: async (method, path, parameters) => {
const host = window.location.protocol + '//' + window.location.host;
const auth = conn.options;
try {
// Refresh token if it will expire in 30 seconds
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
const accessToken = await window.refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
}
return await hassCallApi(host, auth, method, path, parameters);
} catch (err) {
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
// If we connect with access token and get 401, refresh token and try again
const accessToken = await window.refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
return await hassCallApi(host, auth, method, path, parameters);
}
},
// For messages that do not get a response
sendWS: (msg) => {
// eslint-disable-next-line
if (__DEV__) console.log('Sending', msg);
conn.sendMessage(msg);
},
// For messages that expect a response
callWS: (msg) => {
/* eslint-disable no-console */
if (__DEV__) console.log('Sending', msg);
const resp = conn.sendMessagePromise(msg);
if (__DEV__) {
resp.then(
result => console.log('Received', result),
err => console.log('Error', err),
);
}
// In the future we'll do this as a breaking change
// inside home-assistant-js-websocket
return resp.then(result => result.result);
},
}, this.$.storage.getStoredState());
var reconnected = () => {
this._updateHass({ connected: true });
this.loadBackendTranslations();
this._loadPanels();
};
const disconnected = () => {
this._updateHass({ connected: false });
};
conn.addEventListener('ready', reconnected);
// If we reconnect after losing connection and access token is no longer
// valid.
conn.addEventListener('reconnect-error', async (_conn, err) => {
if (err !== ERR_INVALID_AUTH) return;
disconnected();
this.unsubConnection();
const accessToken = await window.refreshToken();
this.handleConnectionPromise(window.createHassConnection(null, accessToken));
});
conn.addEventListener('disconnected', disconnected);
let unsubEntities;
subscribeEntities(conn, (states) => {
this._updateHass({ states: states });
}).then(function (unsub) {
unsubEntities = unsub;
});
let unsubConfig;
subscribeConfig(conn, (config) => {
this._updateHass({ config: config });
}).then(function (unsub) {
unsubConfig = unsub;
});
this._loadPanels();
let unsubThemes;
this.hass.callWS({
type: 'frontend/get_themes',
}).then((themes) => {
this._updateHass({ themes });
applyThemesOnElement(
document.documentElement,
themes,
this.hass.selectedTheme,
true
);
});
// only for new auth
if (conn.options.accessToken) {
this.hass.callWS({
type: 'auth/current_user',
}).then(user => this._updateHass({ user }), () => {});
}
conn.subscribeEvents((event) => {
this._updateHass({ themes: event.data });
applyThemesOnElement(
document.documentElement,
event.data,
this.hass.selectedTheme,
true
);
}, 'themes_updated').then(function (unsub) {
unsubThemes = unsub;
});
this.loadBackendTranslations();
this.unsubConnection = function () {
conn.removeEventListener('ready', reconnected);
conn.removeEventListener('disconnected', disconnected);
unsubEntities();
unsubConfig();
unsubThemes();
};
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || 'states';
}
panelUrlChanged(newPanelUrl) {
this._updateHass({ panelUrl: newPanelUrl });
this.loadTranslationFragment(newPanelUrl);
}
async handleConnectionPromise(prom) {
if (!prom) return;
try {
this.connection = await prom;
} catch (err) {
this.connectionPromise = null;
}
}
handleMoreInfo(ev) {
ev.stopPropagation();
this._updateHass({ moreInfoEntityId: ev.detail.entityId });
}
handleDockSidebar(ev) {
ev.stopPropagation();
this._updateHass({ dockedSidebar: ev.detail.dock });
this.$.storage.storeState();
}
handleNotification(ev) {
this.$.notifications.showNotification(ev.detail.message);
}
handleLogout() {
this.connection.close();
localStorage.clear();
document.location = '/';
}
setTheme(event) {
this._updateHass({ selectedTheme: event.detail });
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme,
true
);
this.$.storage.storeState();
}
selectLanguage(event) {
this._updateHass({ selectedLanguage: event.detail.language });
this.$.storage.storeState();
this.loadResources();
this.loadBackendTranslations();
this.loadTranslationFragment(this.panelUrl);
}
loadTranslationFragment(panelUrl) {
if (translationMetadata.fragments.includes(panelUrl)) {
this.loadResources(panelUrl);
}
}
async _loadPanels() {
const panels = await this.hass.callWS({
type: 'get_panels'
});
this._updateHass({ panels });
}
_updateHass(obj) {
this.hass = Object.assign({}, this.hass, obj);
}
}
customElements.define('home-assistant', HomeAssistant);

View File

@ -37,7 +37,7 @@ function redirectLogin() {
document.location = `${__PUBLIC_PATH__}authorize.html?response_type=code&client_id=${encodeURIComponent(clientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
}
window.refreshToken = () =>
window.refreshToken = () => (window.tokens ?
refreshToken_(clientId(), window.tokens.refresh_token).then((accessTokenResp) => {
window.tokens = Object.assign({}, window.tokens, accessTokenResp);
localStorage.tokens = JSON.stringify(window.tokens);
@ -45,7 +45,7 @@ window.refreshToken = () =>
access_token: accessTokenResp.access_token,
expires: window.tokens.expires
};
}, () => redirectLogin());
}, () => redirectLogin()) : redirectLogin());
function resolveCode(code) {
fetchToken(clientId(), code).then((tokens) => {

View File

@ -0,0 +1,25 @@
import { clearState } from '../../util/ha-pref-storage.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-logout', () => this._handleLogout());
}
hassConnected() {
super.hassConnected();
// only for new auth
if (this.hass.connection.options.accessToken) {
this.hass.callWS({
type: 'auth/current_user',
}).then(user => this._updateHass({ user }), () => {});
}
}
_handleLogout() {
this.hass.connection.close();
clearState();
document.location.href = '/';
}
};

View File

@ -0,0 +1,199 @@
import {
ERR_INVALID_AUTH,
subscribeEntities,
subscribeConfig,
} from 'home-assistant-js-websocket';
import translationMetadata from '../../../build-translations/translationMetadata.json';
import LocalizeMixin from '../../mixins/localize-mixin.js';
import EventsMixin from '../../mixins/events-mixin.js';
import { getState } from '../../util/ha-pref-storage.js';
import { getActiveTranslation } from '../../util/hass-translation.js';
import hassCallApi from '../../util/hass-call-api.js';
import computeStateName from '../../common/entity/compute_state_name.js';
export default superClass =>
class extends EventsMixin(LocalizeMixin(superClass)) {
constructor() {
super();
this.unsubFuncs = [];
}
ready() {
super.ready();
this.addEventListener('try-connection', e =>
this._handleNewConnProm(e.detail.connProm));
if (window.hassConnection) {
this._handleNewConnProm(window.hassConnection);
}
}
async _handleNewConnProm(connProm) {
this.connectionPromise = connProm;
let conn;
try {
conn = await connProm;
} catch (err) {
this.connectionPromise = null;
return;
}
this._setConnection(conn);
}
_setConnection(conn) {
this.hass = Object.assign({
connection: conn,
connected: true,
states: null,
config: null,
themes: null,
panels: null,
panelUrl: this.panelUrl,
language: getActiveTranslation(),
// If resources are already loaded, don't discard them
resources: (this.hass && this.hass.resources) || null,
translationMetadata: translationMetadata,
dockedSidebar: false,
moreInfoEntityId: null,
callService: async (domain, service, serviceData = {}) => {
try {
await conn.callService(domain, service, serviceData);
let message;
let name;
if (serviceData.entity_id && this.hass.states &&
this.hass.states[serviceData.entity_id]) {
name = computeStateName(this.hass.states[serviceData.entity_id]);
}
if (service === 'turn_on' && serviceData.entity_id) {
message = this.localize(
'ui.notification_toast.entity_turned_on',
'entity', name || serviceData.entity_id
);
} else if (service === 'turn_off' && serviceData.entity_id) {
message = this.localize(
'ui.notification_toast.entity_turned_off',
'entity', name || serviceData.entity_id
);
} else {
message = this.localize(
'ui.notification_toast.service_called',
'service', `${domain}/${service}`
);
}
this.fire('hass-notification', { message });
} catch (err) {
const message = this.localize(
'ui.notification_toast.service_call_failed',
'service', `${domain}/${service}`
);
this.fire('hass-notification', { message });
throw err;
}
},
callApi: async (method, path, parameters) => {
const host = window.location.protocol + '//' + window.location.host;
const auth = conn.options;
try {
// Refresh token if it will expire in 30 seconds
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
const accessToken = await window.refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
}
return await hassCallApi(host, auth, method, path, parameters);
} catch (err) {
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
// If we connect with access token and get 401, refresh token and try again
const accessToken = await window.refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
return await hassCallApi(host, auth, method, path, parameters);
}
},
// For messages that do not get a response
sendWS: (msg) => {
// eslint-disable-next-line
if (__DEV__) console.log('Sending', msg);
conn.sendMessage(msg);
},
// For messages that expect a response
callWS: (msg) => {
/* eslint-disable no-console */
if (__DEV__) console.log('Sending', msg);
const resp = conn.sendMessagePromise(msg);
if (__DEV__) {
resp.then(
result => console.log('Received', result),
err => console.log('Error', err),
);
}
// In the future we'll do this as a breaking change
// inside home-assistant-js-websocket
return resp.then(result => result.result);
},
}, getState());
this.hassConnected();
}
hassConnected() {
super.hassConnected();
const conn = this.hass.connection;
const reconnected = () => this.hassReconnected();
const disconnected = () => this._updateHass({ connected: false });
const reconnectError = async (_conn, err) => {
if (err !== ERR_INVALID_AUTH) return;
disconnected();
while (this.unsubFuncs.length) {
this.unsubFuncs.pop()();
}
const accessToken = await window.refreshToken();
this._handleNewConnProm(window.createHassConnection(null, accessToken));
};
conn.addEventListener('ready', reconnected);
conn.addEventListener('disconnected', disconnected);
// If we reconnect after losing connection and access token is no longer
// valid.
conn.addEventListener('reconnect-error', reconnectError);
this.unsubFuncs.push(() => {
conn.removeEventListener('ready', reconnected);
conn.removeEventListener('disconnected', disconnected);
conn.removeEventListener('reconnect-error', reconnectError);
});
subscribeEntities(conn, states => this._updateHass({ states }))
.then(unsub => this.unsubFuncs.push(unsub));
subscribeConfig(conn, config => this._updateHass({ config }))
.then(unsub => this.unsubFuncs.push(unsub));
this._loadPanels();
}
hassReconnected() {
super.hassReconnected();
this._updateHass({ connected: true });
this._loadPanels();
}
async _loadPanels() {
const panels = await this.hass.callWS({
type: 'get_panels'
});
this._updateHass({ panels });
}
};

View File

@ -0,0 +1,23 @@
export default superClass =>
class extends superClass {
ready() {
super.ready();
this.addEventListener('register-dialog', e => this.registerDialog(e.detail));
}
registerDialog({ dialogShowEvent, dialogTag, dialogImport }) {
let loaded = null;
this.addEventListener(dialogShowEvent, (showEv) => {
if (!loaded) {
loaded = dialogImport().then(() => {
const dialogEl = document.createElement(dialogTag);
this.shadowRoot.appendChild(dialogEl);
this.provideHass(dialogEl);
return dialogEl;
});
}
loaded.then(dialogEl => dialogEl.showDialog(showEv.detail));
});
}
};

View File

@ -0,0 +1,35 @@
/* eslint-disable no-unused-vars */
export default superClass => class extends superClass {
constructor() {
super();
this.__pendingHass = false;
this.__provideHass = [];
}
// Exists so all methods can safely call super method
hassConnected() {}
hassReconnected() {}
panelUrlChanged(newPanelUrl) {}
hassChanged(hass, oldHass) {
this.__provideHass.forEach((el) => {
el.hass = hass;
});
}
provideHass(el) {
this.__provideHass.push(el);
}
async _updateHass(obj) {
const oldHass = this.hass;
this.hass = Object.assign({}, this.hass, obj);
this.__pendingHass = true;
await 0;
if (!this.__pendingHass) return;
this.__pendingHass = false;
this.hassChanged(this.hass, oldHass);
}
};

View File

@ -0,0 +1,111 @@
import '@polymer/app-route/app-location.js';
import '@polymer/app-route/app-route.js';
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import '../../layouts/home-assistant-main.js';
import '../../resources/ha-style.js';
import registerServiceWorker from '../../util/register-service-worker.js';
import HassBaseMixin from './hass-base-mixin.js';
import AuthMixin from './auth-mixin.js';
import TranslationsMixin from './translations-mixin.js';
import ThemesMixin from './themes-mixin.js';
import MoreInfoMixin from './more-info-mixin.js';
import SidebarMixin from './sidebar-mixin.js';
import DialogManagerMixin from './dialog-manager-mixin.js';
import ConnectionMixin from './connection-mixin.js';
import NotificationMixin from './notification-mixin.js';
import(/* webpackChunkName: "login-form" */ '../../layouts/login-form.js');
const ext = (baseClass, mixins) => mixins.reduceRight((base, mixin) => mixin(base), baseClass);
class HomeAssistant extends ext(PolymerElement, [
AuthMixin,
ThemesMixin,
TranslationsMixin,
MoreInfoMixin,
SidebarMixin,
ConnectionMixin,
NotificationMixin,
DialogManagerMixin,
HassBaseMixin
]) {
static get template() {
return html`
<app-location route="{{route}}"></app-location>
<app-route
route="{{route}}"
pattern="/:panel"
data="{{routeData}}"
></app-route>
<template is="dom-if" if="[[showMain]]" restamp>
<home-assistant-main
hass="[[hass]]"
route="{{route}}"
></home-assistant-main>
</template>
<template is="dom-if" if="[[!showMain]]" restamp>
<login-form
hass="[[hass]]"
connection-promise="[[connectionPromise]]"
show-loading="[[computeShowLoading(connectionPromise, hass)]]"
></login-form>
</template>
`;
}
static get properties() {
return {
connectionPromise: {
type: Object,
value: null,
},
hass: {
type: Object,
value: null,
},
showMain: {
type: Boolean,
computed: 'computeShowMain(hass)',
},
route: Object,
routeData: Object,
panelUrl: {
type: String,
computed: 'computePanelUrl(routeData)',
observer: 'panelUrlChanged',
},
};
}
ready() {
super.ready();
afterNextRender(null, registerServiceWorker);
}
computeShowMain(hass) {
return hass && hass.states && hass.config && hass.panels;
}
computeShowLoading(connectionPromise, hass) {
// Show loading when connecting or when connected but not all pieces loaded yet
return (connectionPromise != null
|| (hass && hass.connection && (!hass.states || !hass.config)));
}
computePanelUrl(routeData) {
return (routeData && routeData.panel) || 'states';
}
panelUrlChanged(newPanelUrl) {
super.panelUrlChanged(newPanelUrl);
this._updateHass({ panelUrl: newPanelUrl });
}
}
customElements.define('home-assistant', HomeAssistant);

View File

@ -0,0 +1,22 @@
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
export default superClass =>
class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-more-info', e => this._handleMoreInfo(e));
// Load it once we are having the initial rendering done.
afterNextRender(null, () =>
import(/* webpackChunkName: "more-info-dialog" */ '../../dialogs/ha-more-info-dialog.js'));
}
async _handleMoreInfo(ev) {
if (!this.__moreInfoEl) {
this.__moreInfoEl = document.createElement('ha-more-info-dialog');
this.shadowRoot.appendChild(this.__moreInfoEl);
this.provideHass(this.__moreInfoEl);
}
this._updateHass({ moreInfoEntityId: ev.detail.entityId });
}
};

View File

@ -0,0 +1,11 @@
export default superClass =>
class extends superClass {
ready() {
super.ready();
this.registerDialog({
dialogShowEvent: 'hass-notification',
dialogTag: 'notification-manager',
dialogImport: () => import(/* webpackChunkName: "notification-manager" */ '../../managers/notification-manager.js'),
});
}
};

View File

@ -0,0 +1,15 @@
import { storeState } from '../../util/ha-pref-storage.js';
export default superClass =>
class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-dock-sidebar', e =>
this._handleDockSidebar(e));
}
_handleDockSidebar(ev) {
this._updateHass({ dockedSidebar: ev.detail.dock });
storeState(this.hass);
}
};

View File

@ -0,0 +1,46 @@
import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js';
import { storeState } from '../../util/ha-pref-storage.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('settheme', e => this._setTheme(e));
}
hassConnected() {
super.hassConnected();
this.hass.callWS({
type: 'frontend/get_themes',
}).then((themes) => {
this._updateHass({ themes });
applyThemesOnElement(
document.documentElement,
themes,
this.hass.selectedTheme,
true
);
});
this.hass.connection.subscribeEvents((event) => {
this._updateHass({ themes: event.data });
applyThemesOnElement(
document.documentElement,
event.data,
this.hass.selectedTheme,
true
);
}, 'themes_updated').then(unsub => this.unsubFuncs.push(unsub));
}
_setTheme(event) {
this._updateHass({ selectedTheme: event.detail });
applyThemesOnElement(
document.documentElement,
this.hass.themes,
this.hass.selectedTheme,
true
);
storeState(this.hass);
}
};

View File

@ -0,0 +1,80 @@
import translationMetadata from '../../../build-translations/translationMetadata.json';
import { getTranslation } from '../../util/hass-translation.js';
import { storeState } from '../../util/ha-pref-storage.js';
/*
* superClass needs to contain `this.hass` and `this._updateHass`.
*/
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-language-select', e => this._selectLanguage(e));
this._loadResources();
}
hassConnected() {
super.hassConnected();
this._loadBackendTranslations();
}
hassReconnected() {
super.hassReconnected();
this._loadBackendTranslations();
}
panelUrlChanged(newPanelUrl) {
super.panelUrlChanged(newPanelUrl);
this._loadTranslationFragment(newPanelUrl);
}
async _loadBackendTranslations() {
if (!this.hass.language) return;
const language = this.hass.selectedLanguage || this.hass.language;
const { resources } = await this.hass.callWS({
type: 'frontend/get_translations',
language,
});
// If we've switched selected languages just ignore this response
if ((this.hass.selectedLanguage || this.hass.language) !== language) return;
this._updateResources(language, resources);
}
_loadTranslationFragment(panelUrl) {
if (translationMetadata.fragments.includes(panelUrl)) {
this._loadResources(panelUrl);
}
}
async _loadResources(fragment) {
const result = await getTranslation(fragment);
this._updateResources(result.language, result.data);
}
_updateResources(language, data) {
// Update the language in hass, and update the resources with the newly
// loaded resources. This merges the new data on top of the old data for
// this language, so that the full translation set can be loaded across
// multiple fragments.
this._updateHass({
language: language,
resources: {
[language]: Object.assign({}, this.hass
&& this.hass.resources && this.hass.resources[language], data),
},
});
}
_selectLanguage(event) {
this._updateHass({ selectedLanguage: event.detail.language });
storeState(this.hass);
this._loadResources();
this._loadBackendTranslations();
this._loadTranslationFragment(this.panelUrl);
}
};

View File

@ -14,7 +14,6 @@ import EventsMixin from '../mixins/events-mixin.js';
import NavigateMixin from '../mixins/navigate-mixin.js';
import(/* webpackChunkName: "ha-sidebar" */ '../components/ha-sidebar.js');
import(/* webpackChunkName: "more-info-dialog" */ '../dialogs/ha-more-info-dialog.js');
import(/* webpackChunkName: "voice-command-dialog" */ '../dialogs/ha-voice-command-dialog.js');
const NON_SWIPABLE_PANELS = ['kiosk', 'map'];
@ -36,7 +35,6 @@ class HomeAssistantMain extends NavigateMixin(EventsMixin(PolymerElement)) {
height: 100%;
}
</style>
<ha-more-info-dialog hass="[[hass]]"></ha-more-info-dialog>
<ha-url-sync hass="[[hass]]"></ha-url-sync>
<app-route route="{{route}}" pattern="/states" tail="{{statesRouteTail}}"></app-route>
<ha-voice-command-dialog hass="[[hass]]" id="voiceDialog"></ha-voice-command-dialog>

View File

@ -9,11 +9,12 @@ import { ERR_CANNOT_CONNECT, ERR_INVALID_AUTH } from 'home-assistant-js-websocke
import LocalizeMixin from '../mixins/localize-mixin.js';
import EventsMixin from '../mixins/events-mixin.js';
/*
* @appliesMixin LocalizeMixin
*/
class LoginForm extends LocalizeMixin(PolymerElement) {
class LoginForm extends EventsMixin(LocalizeMixin(PolymerElement)) {
static get template() {
return html`
<style include="iron-flex iron-positioning"></style>
@ -114,10 +115,6 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
this.addEventListener('keydown', ev => this.passwordKeyDown(ev));
}
connectedCallback() {
super.connectedCallback();
}
computeLoadingMsg(isValidating) {
return isValidating ? 'Connecting' : 'Loading data';
}
@ -150,10 +147,11 @@ class LoginForm extends LocalizeMixin(PolymerElement) {
validatePassword() {
var auth = this.password;
this.$.hideKeyboardOnFocus.focus();
this.connectionPromise = window.createHassConnection(auth);
const connProm = window.createHassConnection(auth);
this.fire('try-connection', { connProm });
if (this.$.rememberLogin.checked) {
this.connectionPromise.then(function () {
connProm.then(function () {
localStorage.authToken = auth;
});
}

View File

@ -84,7 +84,7 @@ class NotificationManager extends LocalizeMixin(PolymerElement) {
this.$.connToast.classList.toggle('fit-bottom', ev.matches);
}
showNotification(message) {
showDialog({ message }) {
this.$.toast.show(message);
}
}

View File

@ -1,47 +1,32 @@
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
const STORED_STATE = [
'dockedSidebar',
'selectedTheme',
'selectedLanguage',
];
class HaPrefStorage extends PolymerElement {
static get properties() {
return {
hass: Object,
storage: {
type: Object,
value: window.localStorage || {},
},
};
}
storeState() {
if (!this.hass) return;
try {
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
var value = this.hass[key];
this.storage[key] = JSON.stringify(value === undefined ? null : value);
}
} catch (err) {
// Safari throws exception in private mode
}
}
getStoredState() {
var state = {};
const STORED_STATE = ['dockedSidebar', 'selectedTheme', 'selectedLanguage'];
const STORAGE = window.localStorage || {};
export function storeState(hass) {
try {
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
if (key in this.storage) {
state[key] = JSON.parse(this.storage[key]);
}
var value = hass[key];
STORAGE[key] = JSON.stringify(value === undefined ? null : value);
}
return state;
} catch (err) {
// Safari throws exception in private mode
}
}
customElements.define('ha-pref-storage', HaPrefStorage);
export function getState() {
var state = {};
for (var i = 0; i < STORED_STATE.length; i++) {
var key = STORED_STATE[i];
if (key in STORAGE) {
state[key] = JSON.parse(STORAGE[key]);
}
}
return state;
}
export function clearState() {
// STORAGE is an object if localStorage not available.
if (STORAGE.clear) STORAGE.clear();
}