From 72aecfa24e37288e0e45fd1e51300c695fe4b8f2 Mon Sep 17 00:00:00 2001 From: Viet Ngoc Date: Wed, 13 Nov 2024 17:48:29 +0100 Subject: [PATCH] refactor: Add LocationAddress type for location information --- src/css/editor.css | 19 ++- src/editor.ts | 276 +++++++++++++++++++--------------------- src/lunar-phase-card.ts | 63 +++++---- src/types.ts | 6 + src/utils/ha-helper.ts | 46 +++---- src/utils/helpers.ts | 99 +++++++++++++- src/utils/loader.ts | 101 ++++++++++++++- 7 files changed, 410 insertions(+), 200 deletions(-) diff --git a/src/css/editor.css b/src/css/editor.css index 0f5e182..a4c036a 100644 --- a/src/css/editor.css +++ b/src/css/editor.css @@ -16,7 +16,6 @@ border-bottom: inherit; min-height: 48px; align-items: center; - cursor: pointer; overflow: hidden; font-weight: 500; outline: 0px; @@ -98,6 +97,24 @@ ha-select { cursor: pointer; } +.location-item { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: stretch; + padding-inline: 8px; +} + +.location-item .primary { + font-weight: 500; +} + +.location-item .secondary { + color: var(--secondary-text-color); + text-transform: capitalize; +} + + img.bg-thumbnail { width: 70%; height: auto; diff --git a/src/editor.ts b/src/editor.ts index 4111838..48a61ad 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,19 +1,25 @@ /* @typescript-eslint/no-explicit-any */ import { LitElement, html, TemplateResult, css, CSSResultGroup, PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; // Custom card helpers import { fireEvent, LovelaceCardEditor } from 'custom-card-helpers'; -import type { HorizonGraphConfig } from './types'; - import { CARD_VERSION, FONTCOLORS, FONTSTYLES, FONTSIZES } from './const'; import { CUSTOM_BG } from './const'; import editorcss from './css/editor.css'; import { languageOptions, localize } from './localize/localize'; -import { HomeAssistantExtended as HomeAssistant, LunarPhaseCardConfig, FontCustomStyles, defaultConfig } from './types'; -import { deepMerge, InitializeDefaultConfig } from './utils/ha-helper'; -import { loadHaComponents, stickyPreview } from './utils/loader'; +import { + HomeAssistantExtended as HomeAssistant, + LunarPhaseCardConfig, + FontCustomStyles, + defaultConfig, + LocationAddress, +} from './types'; +import { generateConfig } from './utils/ha-helper'; +import { compareConfig, getAddressFromOpenStreet } from './utils/helpers'; +import { loadHaComponents, stickyPreview, _saveConfig } from './utils/loader'; @customElement('lunar-phase-card-editor') export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEditor { @@ -21,62 +27,93 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit @state() private _config!: LunarPhaseCardConfig; @state() private _activeTabIndex?: number; + @state() _activeGraphEditor = false; - public async setConfig(config: LunarPhaseCardConfig): Promise { - this._config = config; - } + @state() private _location!: LocationAddress; - private get selectedLanguage(): string { - return this._config?.selected_language || this.hass?.language; + public async setConfig(config: LunarPhaseCardConfig): Promise { + this._config = { ...config }; } - private localize = (string: string, search = '', replace = ''): string => { - return localize(string, this.selectedLanguage, search, replace); - }; - - connectedCallback() { + connectedCallback(): void { super.connectedCallback(); void loadHaComponents(); void stickyPreview(); if (process.env.ROLLUP_WATCH === 'true') { window.LunarEditor = this; } - this._cleanConfig(); + console.log('LunarPhaseCardEditor connected'); } disconnectedCallback(): void { super.disconnectedCallback(); + console.log('LunarPhaseCardEditor disconnected'); + } + + protected async firstUpdated(changedProps: PropertyValues): Promise { + super.firstUpdated(changedProps); + await new Promise((resolve) => setTimeout(resolve, 0)); + this._handleFirstConfig(this._config); + this.getLocation(); } protected updated(_changedProperties: PropertyValues): void { super.updated(_changedProperties); - if (_changedProperties.has('_activeTabIndex') && this._activeTabIndex !== undefined) { + if (_changedProperties.has('_activeTabIndex')) { if (this._activeTabIndex === 3) { - this._activeTabChanged(); + this._activeGraphEditor = true; + this._toggleGraphEditor(); } else { - this._cleanConfig(); + this._activeGraphEditor = false; } + this.setLocalStorageItem('activeGraphEditor', this._activeGraphEditor); } } - private _activeTabChanged(): void { - const event = new CustomEvent('lunar-card-event', { + private async _handleFirstConfig(config: LunarPhaseCardConfig): Promise { + const isValid = compareConfig({ ...defaultConfig }, { ...config }); + if (!isValid) { + console.log('Invalid config, generating new config'); + const cardId = `lmc-${Math.random().toString(36).substring(2, 9)}`; + const newConfig = generateConfig({ ...config }); + newConfig.cardId ?? (newConfig.cardId = cardId); + this._config = newConfig; + fireEvent(this, 'config-changed', { config: this._config }); + await _saveConfig(cardId, this._config); + } + } + private get selectedLanguage(): string { + return this._config?.selected_language || this.hass?.language; + } + + private localize = (string: string, search = '', replace = ''): string => { + return localize(string, this.selectedLanguage, search, replace); + }; + + private setLocalStorageItem(key: string, value: any): void { + localStorage.setItem(key, JSON.stringify(value)); + } + + private getLocation = () => { + this.updateComplete.then(() => { + const { latitude, longitude } = this._config; + if (latitude && longitude) { + getAddressFromOpenStreet(latitude, longitude).then((location) => { + this._location = location; + }); + } + }); + }; + + private _toggleGraphEditor(): void { + const event = new CustomEvent('toggle-graph-editor', { + detail: { + activeGraphEditor: this._activeGraphEditor, + }, bubbles: true, composed: true, - detail: { activeTabIndex: this._activeTabIndex }, }); this.dispatchEvent(event); - console.log('dispatched event', this._activeTabIndex); - } - - private _cleanConfig(): void { - if (!this._config) { - return; - } - if (this._config?.activeGraphTab !== undefined) { - this._config = { ...this._config, activeGraphTab: undefined }; - fireEvent(this, 'config-changed', { config: this._config }); - } } /* --------------------------------- RENDERS -------------------------------- */ @@ -88,22 +125,18 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit const tabsConfig = [ { - key: 'baseConfig', label: 'Lat & Long', content: this._renderBaseConfigSelector(), }, { - key: 'viewConfig', label: 'View', content: this._renderViewConfiguration(), }, { - key: 'fontOptions', label: 'Font', content: this._renderFontConfiguration(), }, { - key: 'graphConfig', label: 'Graph', content: this._renderGraphConfig(), }, @@ -118,14 +151,14 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit private _renderBaseConfigSelector(): TemplateResult { const radiosOptions = this._getBaseConfigSelector().options; - const radios = radiosOptions.map((item) => { return html` `; @@ -141,6 +174,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit const contentWrapp = html`
${radios} ${southern}
+ ${this._renderLocation()}
${this._config?.use_default ? this._renderUseDefault() @@ -155,6 +189,18 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit return this.contentTemplate('baseConfig', 'baseConfig', 'mdi:cog', contentWrapp); } + private _renderLocation(): TemplateResult { + const location = this._location || { country: '', city: '' }; + return html` +
+
+
${location.country} ${unsafeHTML(`•`)} ${location.city}
+
+ +
+ `; + } + private _renderUseDefault(): TemplateResult { const latLong = [ { label: 'Latitude', value: this._config?.latitude }, @@ -170,8 +216,8 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit private _renderCustomLatLong(): TemplateResult { const latLong = [ - { label: this.localize('editor.placeHolder.latitude'), configKey: 'latitude' }, - { label: this.localize('editor.placeHolder.longitude'), configKey: 'longitude' }, + { label: this.localize('editor.placeHolder.latitude'), configValue: 'latitude' }, + { label: this.localize('editor.placeHolder.longitude'), configValue: 'longitude' }, ]; return html`
@@ -179,8 +225,9 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit return html` `; @@ -206,7 +253,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit .hass=${this.hass} .value=${this._config?.entity} .configValue=${'entity'} - @value-changed=${this._entityChanged} + @value-changed=${this._handleValueChange} .label=${'Entity'} .required=${false} .includeEntities=${combinedEntities} @@ -215,7 +262,10 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit `; const entitySelected = this._config?.entity ? true : false; - const entityLatLong = this._getEntityLatLong(); + const entityLatLong = [ + { label: 'Latitude', value: this._config?.latitude }, + { label: 'Longitude', value: this._config?.longitude }, + ]; const entityContent = html`
${entityLatLong.map((item) => { @@ -524,7 +574,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit }: { activeTabIndex: number; onTabChange: (index: number) => void; - tabs: { content: TemplateResult; icon?: string; key: string; label: string; stacked?: boolean }[]; + tabs: { content: TemplateResult; icon?: string; key?: string; label: string; stacked?: boolean }[]; }): TemplateResult => { return html` onTabChange((e.target as any).activeIndex)}> @@ -605,39 +655,30 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit const target = ev.target as any; const configValue = target?.configValue; - const configKey = target?.configKey; + let configKey = target?.configKey || ev.detail?.configKey; - let value: any = target.checked !== undefined ? target.checked : ev.detail.value; + let value: any = target.checked !== undefined ? target.checked : ev.detail?.value; - console.log('configKey', configKey, 'configValue', configValue, 'value', value); + // console.log('configKey', configKey, 'configValue', configValue, 'value', value); const updates: Partial = {}; - // Define default values for FontCustomStyles - const defaultFontCustomStyles: FontCustomStyles = { - header_font_size: 'x-large', - header_font_style: 'capitalize', - header_font_color: '', - label_font_size: 'auto', - label_font_style: 'none', - label_font_color: '', - hide_label: false, - }; - // Check if the configValue is a key of FontCustomStyles if (configKey === 'font_customize') { const key = configValue as keyof FontCustomStyles; // If the current value is undefined, use the default value - const updatedValue = value !== undefined ? value : defaultFontCustomStyles[key]; + const updatedValue = value !== undefined ? value : defaultConfig[key]; updates.font_customize = { ...this._config.font_customize, [key]: updatedValue, }; } else if (configKey === 'graph_config') { + value = target.checked !== undefined ? target.checked : ev.detail?.value; updates.graph_config = { ...this._config.graph_config, [configValue]: value, }; + this._activeGraphEditor = true; } else if (configValue === 'selected_language') { if (value === 'system' || value === undefined) { updates.selected_language = this.hass?.language; @@ -647,79 +688,42 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit } else if (configKey === 'custom_bg') { value = event.target.value; updates.custom_background = value === 0 ? undefined : CUSTOM_BG[value]; + } else if (configValue === 'entity') { + const attribute = this.hass.states[value].attributes; + updates.latitude = attribute.latitude ?? attribute.location.latitude; + updates.longitude = attribute.longitude ?? attribute.location.longitude; + updates.entity = value; + console.log('updates', updates); + } else if (configKey === 'location') { + value = event.target.value; + const radiosOptions = this._getBaseConfigSelector().options.map((item: { key: string }) => item.key); + radiosOptions.forEach((item: string) => { + if (item === value) { + updates[item] = true; + } else { + updates[item] = false; + } + }); + if (value === 'use_custom') { + updates.entity = ''; + } else if (value === 'use_default') { + updates.entity = ''; + updates.latitude = this.hass.config.latitude; + updates.longitude = this.hass.config.longitude; + } + } else if (['latitude', 'longitude'].includes(configValue)) { + value = event.target?.value; + updates[configValue] = value.trim(); } else { // Update the main configuration object updates[configValue] = value; } - if (Object.keys(updates).length > 0) { this._config = { ...this._config, ...updates }; - if (this._activeTabIndex === 3) { - this._config = { ...this._config, activeGraphTab: this._activeTabIndex }; - } else { - this._config = { ...this._config, activeGraphTab: undefined }; - } fireEvent(this, 'config-changed', { config: this._config }); } - } - - private _getEntityLatLong(): { label: string; value: number | string }[] { - if (!this._config?.entity) { - return []; - } - - const entity = this.hass.states[this._config.entity]; - if (!entity || !entity.attributes) { - return []; - } - - return [ - { - label: 'Latitude', - value: entity.attributes.latitude ?? entity.attributes.location.latitude, - }, - { - label: 'Longitude', - value: entity.attributes.longitude ?? entity.attributes.location.longitude, - }, - ]; - } - - private _handleRadioChange(ev): void { - if (!this._config || !this.hass) { - return; - } - const target = ev.target; - const configValue = target.value; - - if (this._config[configValue] === true) { - return; - } - - const updates: Partial = {}; - const radiosOptions = this._getBaseConfigSelector().options.map((item) => item.key); - radiosOptions.forEach((item) => { - if (item === configValue) { - updates[item] = true; - } else { - updates[item] = false; - } - }); - - if (configValue === 'use_custom') { - updates.entity = ''; - } - - if (configValue === 'use_default') { - updates.entity = ''; - updates.latitude = this.hass.config.latitude; - updates.longitude = this.hass.config.longitude; - } - - if (Object.keys(updates).length > 0) { - this._config = { ...this._config, ...updates }; - console.log('updates', updates); - fireEvent(this, 'config-changed', { config: this._config }); + if ('latitude' in updates || 'longitude' in updates) { + this.getLocation(); } } @@ -774,7 +778,7 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit } } - private _getBaseConfigSelector(): any { + private _getBaseConfigSelector(): { options: { key: string; label: string }[] } { return { options: [ { @@ -793,24 +797,6 @@ export class LunarPhaseCardEditor extends LitElement implements LovelaceCardEdit }; } - private _entityChanged(ev): void { - if (!this._config || !this.hass) { - return; - } - const entity = ev.detail.value; - if (this._config.entity === entity || !entity) { - return; - } - const attribute = this.hass.states[entity].attributes; - - const latitude = attribute.latitude ?? attribute.location.latitude; - const longitude = attribute.longitude ?? attribute.location.longitude; - - const updates: Partial = { entity, latitude, longitude }; - this._config = { ...this._config, ...updates }; - fireEvent(this, 'config-changed', { config: this._config }); - } - static get styles(): CSSResultGroup { return css` ${editorcss} diff --git a/src/lunar-phase-card.ts b/src/lunar-phase-card.ts index 6a39242..4a67d29 100644 --- a/src/lunar-phase-card.ts +++ b/src/lunar-phase-card.ts @@ -1,4 +1,4 @@ -import { LovelaceCardEditor, formatDate, FrontendLocaleData, TimeFormat } from 'custom-card-helpers'; +import { LovelaceCardEditor, formatDate, FrontendLocaleData, TimeFormat, fireEvent } from 'custom-card-helpers'; import { LitElement, html, TemplateResult, PropertyValues, CSSResultGroup, nothing } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -9,7 +9,9 @@ import { HomeAssistantExtended as HomeAssistant, LunarPhaseCardConfig, defaultCo // Helpers import { BLUE_BG, PageType, MoonState, ICON } from './const'; import { localize } from './localize/localize'; +import { deepMerge, generateConfig } from './utils/ha-helper'; import { getDefaultConfig } from './utils/helpers'; +import { isEditorMode } from './utils/loader'; // components import { LunarBaseData } from './components/moon-data'; @@ -49,6 +51,8 @@ export class LunarPhaseCard extends LitElement { @state() _state: MoonState = MoonState.READY; @state() _resizeInitiated = false; @state() public _cardWidth = 0; + + @state() _activeEditorPage?: PageType; @state() _resizeObserver: ResizeObserver | null = null; @query('lunar-base-data') _data!: LunarBaseData; @query('moon-horizon') _moonHorizon!: MoonHorizon; @@ -71,14 +75,7 @@ export class LunarPhaseCard extends LitElement { throw new Error('Invalid configuration'); } - this.config = { - ...config, - }; - } - - constructor() { - super(); - this._handleEditorEvent = this._handleEditorEvent.bind(this); + this.config = generateConfig({ ...config }); } connectedCallback(): void { @@ -87,17 +84,20 @@ export class LunarPhaseCard extends LitElement { window.LunarCard = this; window.Moon = this.moon; } + if (isEditorMode(this)) { + document.addEventListener('toggle-graph-editor', (ev) => this._handleEditorEvent(ev as CustomEvent)); + } if (!this._resizeInitiated && !this._resizeObserver) { this.delayedAttachResizeObserver(); } this.startRefreshInterval(); - document.addEventListener('lunar-card-event', (ev) => this._handleEditorEvent(ev as CustomEvent)); } disconnectedCallback(): void { this.clearRefreshInterval(); this.detachResizeObserver(); this._resizeInitiated = false; + document.removeEventListener('toggle-graph-editor', (ev) => this._handleEditorEvent(ev as CustomEvent)); super.disconnectedCallback(); } delayedAttachResizeObserver(): void { @@ -135,24 +135,24 @@ export class LunarPhaseCard extends LitElement { } } - private _handleEditorEvent(ev: CustomEvent) { - ev.stopPropagation(); - if (!this.isEditorPreview) { + private _handleEditorEvent(event: CustomEvent) { + event.stopPropagation(); + if (!isEditorMode(this)) { return; } - console.log('editor event', ev.detail); - const activeTabIndex = ev.detail.activeTabIndex; - if (activeTabIndex === 3 && this._activeCard !== PageType.HORIZON) { + + const isHorizon = event.detail.activeGraphEditor; + if (isHorizon) { this._activeCard = PageType.HORIZON; - } else if (activeTabIndex !== 3) { + } else { return; } } protected async firstUpdated(_changedProperties: PropertyValues): Promise { super.firstUpdated(_changedProperties); - this._handleFirstRender(); await new Promise((resolve) => setTimeout(resolve, 0)); + this._handleFirstRender(); this._computeStyles(); this.measureCard(); } @@ -225,22 +225,30 @@ export class LunarPhaseCard extends LitElement { } private _handleFirstRender() { - if (this.isEditorPreview && this.config.activeGraphTab === 3) { - this._activeCard = PageType.HORIZON; - } else if (this.isEditorPreview && !this.config.activeGraphTab && this._defaultCard === PageType.HORIZON) { - this._activeCard = PageType.BASE; - setTimeout(() => { + if (isEditorMode(this)) { + const activeGraphEditor = this.isGraphEditor; + if (activeGraphEditor && this._activeCard !== PageType.HORIZON) { this._activeCard = PageType.HORIZON; - }, 150); + } else if (!activeGraphEditor && this._defaultCard === PageType.HORIZON) { + this._activeCard = PageType.BASE; + setTimeout(() => { + this._activeCard = PageType.HORIZON; + }, 150); + } else { + this._activeCard = this._defaultCard; + } } else { this._activeCard = this._defaultCard; - this.requestUpdate(); } } + private get isGraphEditor(): Boolean { + const value = localStorage.getItem('activeGraphEditor'); + return value === 'true'; + } + private startRefreshInterval() { - // console.log('refresh start', new Date().toLocaleTimeString()); - // Clear any existing interval to avoid multiple intervals running + // Clear any existing interval if (this._refreshInterval !== undefined) { clearInterval(this._refreshInterval); } @@ -289,7 +297,6 @@ export class LunarPhaseCard extends LitElement {
- ${header}
${renderCardMap[card]}
diff --git a/src/types.ts b/src/types.ts index ef0dc13..218ee3a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,6 +80,7 @@ export interface LunarPhaseCardConfig extends LovelaceCardConfig { longitude: number; font_customize: FontCustomStyles; graph_config?: HorizonGraphConfig; + cardId?: string; } export const defaultConfig: Partial = { @@ -159,3 +160,8 @@ export type ChartColors = { fillBelowLineColor: string; [key: string]: string; }; + +export type LocationAddress = { + country: string; + city: string; +}; diff --git a/src/utils/ha-helper.ts b/src/utils/ha-helper.ts index f74e8a4..454ff91 100644 --- a/src/utils/ha-helper.ts +++ b/src/utils/ha-helper.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { LunarPhaseCardConfig } from '../types'; +import { LunarPhaseCardConfig, defaultConfig } from '../types'; export function deepMerge(target: any, source: any): any { for (const key of Object.keys(source)) { @@ -14,26 +14,26 @@ export function deepMerge(target: any, source: any): any { return target; } -export function InitializeDefaultConfig(): Record { - const defaultConfig: Partial = { - type: 'custom:lunar-phase-card', - entity: '', - use_default: true, - use_custom: false, - use_entity: false, - show_background: true, - selected_language: 'en', - compact_view: true, - '12hr_format': false, - font_customize: { - header_font_size: 'x-large', - header_font_style: 'capitalize', - header_font_color: '', - label_font_size: 'auto', - label_font_style: 'none', - label_font_color: '', - hide_label: false, - }, +export const generateConfig = (config: LunarPhaseCardConfig): LunarPhaseCardConfig => { + const defaultConf = defaultConfig; + const { y_ticks, x_ticks } = config; + if (y_ticks !== undefined && x_ticks !== undefined) { + defaultConf.graph_config = { + ...defaultConf.graph_config, + y_ticks, + x_ticks, + }; + config = { ...config, y_ticks: undefined, x_ticks: undefined }; + } + + const conf = { + ...defaultConf, + ...config, + font_customize: { ...defaultConf.font_customize, ...config.font_customize }, + graph_config: { ...defaultConf.graph_config, ...config.graph_config }, }; - return defaultConfig; -} + + return conf; +}; + +export default generateConfig; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index c3d63f5..3eb2429 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,6 @@ -import { FrontendLocaleData, TimeFormat, HomeAssistant } from 'custom-card-helpers'; +import { FrontendLocaleData, TimeFormat, HomeAssistant, LovelaceCardConfig } from 'custom-card-helpers'; +import { LocationAddress, LunarPhaseCardConfig } from '../types'; import { MOON_IMAGES } from './moon-pic'; export function formatMoonTime(dateString: string): string { @@ -168,13 +169,29 @@ export function getDefaultConfig(hass: HomeAssistant) { const selected_language = hass.language; const timeFormat = useAmPm(hass.locale); const mile_unit = length !== 'km'; - console.log('getDefaultConfig', latitude, longitude, selected_language, timeFormat, mile_unit); + const cardId = `lmc-${Math.random().toString(36).substring(2, 9)}`; + console.log( + 'default config', + 'latitude:', + latitude, + 'longitude:', + longitude, + 'selected_language:', + selected_language, + 'timeFormat:', + timeFormat, + 'mile_unit:', + mile_unit, + 'cardId:', + cardId + ); return { latitude, longitude, selected_language, '12hr_format': timeFormat, mile_unit, + cardId, }; } @@ -189,3 +206,81 @@ export const compareTime = (time: Date): boolean => { // if time is between 24ago and 24h from now return hoursDifference >= -24 && hoursDifference <= 24; }; + +export function compareChanges(oldConfig: LunarPhaseCardConfig, newConfig: Partial) { + const changes: Record = {}; + + for (const key of Object.keys(newConfig)) { + if (newConfig[key] instanceof Object && key in oldConfig) { + const nestedChanges = compareChanges(oldConfig[key], newConfig[key]); + // Only add nested changes if there are actual differences + if (nestedChanges && Object.keys(nestedChanges).length > 0) { + changes[key] = nestedChanges; + } + } else if (oldConfig[key] !== newConfig[key]) { + // Only add the key if the values are different + changes[key] = { + oldValue: oldConfig[key], + newValue: newConfig[key], + }; + } + } + + // Log the changes with old vs. new, only if there are changes + if (Object.keys(changes).length > 0) { + console.group('Configuration Changes'); + Object.entries(changes).forEach(([key, value]) => { + if (typeof value === 'object' && value.oldValue !== undefined && value.newValue !== undefined) { + console.log(`%c${key}:`, 'color: #1e88e5', 'Old:', value.oldValue, 'New:', value.newValue); + } else { + console.log(`%c${key}:`, 'color: #1e88e5', value); + } + }); + console.groupEnd(); + } + + return changes; // Ensure we return the changes object +} + +export function compareConfig(refObj: any, configObj: any): boolean { + let isValid = true; + for (const key in refObj) { + if (typeof refObj[key] === 'object' && refObj[key] !== null) { + if (!(key in configObj)) { + isValid = false; + } else if (!compareConfig(refObj[key], configObj[key])) { + isValid = false; // If any nested object is invalid + } + } else { + // Check if the property exists in configObj + if (!(key in configObj)) { + isValid = false; + } + } + } + return isValid; +} + +export async function getAddressFromOpenStreet(lat: number, lon: number): Promise { + console.log('getAddressFromOpenStreet', lat, lon); + const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=jsonv2`; + try { + const response = await fetch(url); + const data = await response.json(); + + if (response.ok) { + // Extract address components from the response + const address = { + city: data.address.city || data.address.town || '', + country: data.address.country || data.address.state || '', + }; + console.log('Address fetched from OpenStreetMap:', address); + return address; + } else { + throw new Error('Failed to fetch address OpenStreetMap'); + } + } catch (error) { + console.log('Error fetching address from OpenStreetMap:', error); + return { city: '', country: '' }; + } +} diff --git a/src/utils/loader.ts b/src/utils/loader.ts index f0fb90d..81ed421 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -1,6 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { REPOSITORY } from '../const'; +import { LovelaceConfig } from 'custom-card-helpers'; +import { REPOSITORY } from '../const'; +import { LunarPhaseCardConfig } from '../types'; +export interface HuiRootElement extends HTMLElement { + lovelace: { + config: LovelaceConfig; + current_view: number; + [key: string]: any; + }; + ___curView: number; +} // Hack to load ha-components needed for editor export const loadHaComponents = () => { if (!customElements.get('ha-form')) { @@ -55,3 +65,92 @@ export async function fetchLatestReleaseTag() { console.error('Error fetching the latest release tag:', error); } } + +export function isCardInEditMode(cardElement: HTMLElement) { + let parent1Class: string | undefined = undefined; + let parent2Class: string | undefined = undefined; + + if (cardElement) { + const parentElement = cardElement.offsetParent; + if (parentElement) { + parent1Class = ((parentElement as HTMLElement).className || '').trim(); + + const parentParentElement = parentElement.parentElement; + if (parentParentElement) { + parent2Class = ((parentParentElement as HTMLElement).className || '').trim(); + } + } + } else { + console.log('Card element not found'); + } + + let inEditMode = false; + if (parent1Class === 'element-preview') { + inEditMode = true; + } else if (parent2Class === 'gui-editor') { + inEditMode = true; + } + console.log('Card in edit mode:', inEditMode); + return inEditMode; +} + +export const getLovelace = () => { + const root = document.querySelector('home-assistant')?.shadowRoot?.querySelector('home-assistant-main')?.shadowRoot; + + const resolver = + root?.querySelector('ha-drawer partial-panel-resolver') || + root?.querySelector('app-drawer-layout partial-panel-resolver'); + + const huiRoot = (resolver?.shadowRoot || resolver) + ?.querySelector('ha-panel-lovelace') + ?.shadowRoot?.querySelector('hui-root'); + + if (huiRoot) { + const ll = huiRoot.lovelace; + ll.current_view = huiRoot.___curView; + return ll; + } + + return null; +}; + +export const getDialogEditor = () => { + const root = document.querySelector('home-assistant')?.shadowRoot; + const editor = root?.querySelector('hui-dialog-edit-card'); + if (editor) { + return editor; + } + return null; +}; + +export async function _saveConfig(cardId: string, config: LunarPhaseCardConfig): Promise { + const lovelace = getLovelace(); + if (!lovelace) { + console.log('Lovelace not found'); + return; + } + const dialogEditor = getDialogEditor() as any; + const currentView = lovelace.current_view; + + const cardConfig = lovelace.config.views[currentView].cards; + + let cardIndex = + cardConfig?.findIndex((card) => card.cardId === cardId) === -1 ? dialogEditor?._params?.cardIndex : -1; + console.log('Card index:', cardIndex); + if (cardIndex === -1) { + console.log('Card not found in the config'); + return; + } + + let newCardConfig = [...(cardConfig || [])]; + newCardConfig[cardIndex] = config; + const newView = { ...lovelace.config.views[currentView], cards: newCardConfig }; + const newViews = [...lovelace.config.views]; + newViews[currentView] = newView; + lovelace.saveConfig({ views: newViews }); + console.log('Saving new config:', newViews[currentView].cards![cardIndex]); +} + +export function isEditorMode(card: HTMLElement) { + return card.offsetParent?.classList.contains('element-preview'); +}