From d659344065dc6b4fcc2884d43a3ffd888b3ab20a Mon Sep 17 00:00:00 2001 From: Todd Lucas Date: Sun, 1 Sep 2024 00:53:41 -0500 Subject: [PATCH] [ 1.0.6 ] * Changed medialist to use a cached list when card is displayed in configuration editor. The medialist will be retrieved once while the card is being edited, and stored in a cache until the card editor is closed. --- CHANGELOG.md | 4 + src/card.ts | 133 ++++++++++++++----------- src/components/media-browser-icons.ts | 3 + src/components/media-browser-list.ts | 3 + src/constants.ts | 5 +- src/editor/base-editor.ts | 109 +++++++------------- src/editor/editor.ts | 83 ++++----------- src/editor/general-editor.ts | 1 + src/sections/pandora-browser.ts | 68 ++++++++++--- src/sections/preset-browser.ts | 82 +++++++++++---- src/sections/recent-browser.ts | 68 ++++++++++--- src/sections/source-browser.ts | 66 ++++++++++-- src/sections/userpreset-browser.ts | 128 +++++++++++++----------- src/services/media-control-service.ts | 99 ------------------ src/services/soundtouchplus-service.ts | 37 +++++++ src/types/configarea.ts | 6 +- src/utils/utils.ts | 29 ++++++ 17 files changed, 510 insertions(+), 414 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eecae4b..858b6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Change are listed in reverse chronological order (newest to oldest). +###### [ 1.0.6 ] - 2024/09/01 + + * Changed medialist to use a cached list when card is displayed in configuration editor. The medialist will be retrieved once while the card is being edited, and stored in a cache until the card editor is closed. + ###### [ 1.0.5 ] - 2024/08/29 * Fixed various card configuration issues, which will make the card easier to configure via the HA UI. diff --git a/src/card.ts b/src/card.ts index 9e13e5c..a1443d2 100644 --- a/src/card.ts +++ b/src/card.ts @@ -71,6 +71,9 @@ export class Card extends LitElement { @state() cancelLoader!: boolean; @state() playerId!: string; + // card editor medialist cache items. + static mediaListCache: { [key: string]: object } = {}; + /** Indicates if createStore method is executing for the first time (true) or not (false). */ private _isFirstTimeSetup: boolean = true; @@ -80,7 +83,10 @@ export class Card extends LitElement { */ constructor() { + // invoke base class method. super(); + + // initialize storage. this.showLoader = false; this.cancelLoader = false; this.loaderTimestamp = 0; @@ -347,10 +353,22 @@ export class Card extends LitElement { // invoke base class method. super.connectedCallback(); + // TODO - while in card edit mode, may be able to prevent loaders from showing + // by removing the PROGRESS_STARTED / PROGRESS_DONE event listeners for all card + // instances besides the editor card. see utils.isCardInEditPreview for code + // that determines how to check for the card in edit mode. + // this will remove all of the duplicate progress loading indicators when + // data is being refreshed in the card editor. + // add event listeners for this control. window.addEventListener(PROGRESS_DONE, this.OnProgressDone); window.addEventListener(PROGRESS_STARTED, this.OnProgressStarted); - window.addEventListener(SECTION_SELECTED, this.OnSectionSelected); + + // only add the following events if card configuration is being edited. + if (isCardInEditPreview(this)) { + window.addEventListener(SECTION_SELECTED, this.OnSectionSelected); + } + } @@ -372,43 +390,11 @@ export class Card extends LitElement { // remove event listeners for this control. window.removeEventListener(PROGRESS_DONE, this.OnProgressDone); window.removeEventListener(PROGRESS_STARTED, this.OnProgressStarted); - window.removeEventListener(SECTION_SELECTED, this.OnSectionSelected); - } + // always remove the following events, as isCardInEditPreview() can sometimes + // return a different value than when the event was added in connectedCallback! + window.removeEventListener(SECTION_SELECTED, this.OnSectionSelected); - /** - * Called when an update was triggered, before rendering. Receives a Map of changed - * properties, and their previous values. This can be used for modifying or setting - * new properties before a render occurs. - */ - protected update(changedProperties: PropertyValues) { - - // invoke base class method. - super.update(changedProperties); - - // console.log("update (card) - update event (pre-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); - } - - - /** - * Called when an update was triggered, after rendering. Receives a Map of changed - * properties, and their previous values. This can be used for observing and acting - * on property changes. - */ - protected updated(changedProperties: PropertyValues) { - - // invoke base class method. - super.updated(changedProperties); - - // console.log("updated (card) - update event (post-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); } @@ -417,10 +403,10 @@ export class Card extends LitElement { * lifetime of an element. Useful for one-time setup work that requires access to * the DOM. */ - protected firstUpdated(_changedProperties: PropertyValues): void { + protected firstUpdated(changedProperties: PropertyValues): void { // invoke base class method. - super.firstUpdated(_changedProperties); + super.firstUpdated(changedProperties); // if there are things that you only want to happen one time when the configuration // is initially loaded, then do them here. @@ -477,10 +463,10 @@ export class Card extends LitElement { this.requestUpdate(); } - // console.log("firstUpdated (card) - first render complete\n- this.section=%s\n- Store.selectedConfigArea=%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // ); + //console.log("firstUpdated (card) - first render complete\n- this.section=%s\n- Store.selectedConfigArea=%s", + // JSON.stringify(this.section || '*undefined*'), + // JSON.stringify(Store.selectedConfigArea), + //); } @@ -495,7 +481,12 @@ export class Card extends LitElement { this.cancelLoader = true; const duration = Date.now() - this.loaderTimestamp; - //console.log("card.OnProgressDone()\nHiding progress indicator - duration=%s, this.showLoader=%s", JSON.stringify(duration), JSON.stringify(this.showLoader)); + //console.log("OnProgressDone (card) - Hiding progress indicator\n- duration=%s\n- this.showLoader=%s\n- isCardInEditPreview=%s", + // JSON.stringify(duration), + // JSON.stringify(this.showLoader), + // JSON.stringify(isCardInEditPreview(this)), + //); + if (this.showLoader) { if (duration < 1000) { setTimeout(() => (this.showLoader = false), 1000 - duration); @@ -520,7 +511,15 @@ export class Card extends LitElement { protected OnProgressStarted = (args: Event) => { //console.log("OnProgressStarted() - Event Args:\n%s", JSON.stringify(args,null,2)); - //console.log("progress - this.showLoader=%s\n this.config.sections=%s\n args section=%s\n this.section=%s", JSON.stringify(this.showLoader), JSON.stringify(this.config.sections), JSON.stringify((event as CustomEvent).detail.section), JSON.stringify(this.section)); + + //console.log("OnProgressStarted (card) - this.showLoader=%s\n- this.config.sections=%s\n- args section=%s\n- this.section=%s\n- isCardInEditPreview=%s", + // JSON.stringify(this.showLoader), + // JSON.stringify(this.config.sections), + // JSON.stringify((args as CustomEvent).detail.section), + // JSON.stringify(this.section), + // JSON.stringify(isCardInEditPreview(this)), + //); + if (!this.showLoader && (!this.config.sections || (args as CustomEvent).detail.section === this.section)) { this.cancelLoader = false; //console.log("progress is about to show"); @@ -531,9 +530,9 @@ export class Card extends LitElement { if (!this.cancelLoader) { this.showLoader = true; this.loaderTimestamp = Date.now(); - // console.log("OnProgressStarted (card) - progress is showing - loaderTimestamp=%s", - // JSON.stringify(this.loaderTimestamp), - // ); + //console.log("OnProgressStarted (card) - progress is showing - loaderTimestamp=%s", + // JSON.stringify(this.loaderTimestamp), + //); } }, 250); } @@ -541,9 +540,10 @@ export class Card extends LitElement { /** - * Handles the `SECTION_SELECTED` event. + * Handles the card configuration editor `SECTION_SELECTED` event. * * This will select a section for display / rendering. + * This event should only be fired from the configuration editor instance. * * @param args Event arguments that contain the section that was selected. */ @@ -553,15 +553,22 @@ export class Card extends LitElement { // is section activated? if so, then select it. if (this.config.sections?.includes(sectionToSelect)) { - //console.log("OnSectionSelected (card) - SECTION_SELECTED event\n- OLD section=%s\n- NEW section=%s", + + //console.log("OnSectionSelected (card) - SECTION_SELECTED event\n- OLD section=%s\n- NEW section=%s\n- store.section=%s", // JSON.stringify(this.section), - // JSON.stringify(sectionToSelect)); + // JSON.stringify(sectionToSelect), + // JSON.stringify(this.store.section), + //); + this.section = sectionToSelect; this.store.section = this.section; + } else { - // console.log("OnSectionSelected (card) - SECTION_SELECTED event\n- Section is not active: %s", - // JSON.stringify(sectionToSelect) - // ); + + //console.log("OnSectionSelected (card) - SECTION_SELECTED event\n- Section is not active: %s", + // JSON.stringify(sectionToSelect) + //); + } } @@ -578,17 +585,22 @@ export class Card extends LitElement { const section = args.detail; if (!this.config.sections || this.config.sections.indexOf(section) > -1) { + //console.log("OnShowSection (card) - SHOW_SECTION event\n- OLD section=%s\n- NEW section=%s", // JSON.stringify(this.section), // JSON.stringify(section) //); + this.section = section; this.store.section = this.section; this.requestUpdate(); + } else { - // console.log("OnShowSection (card) - SHOW_SECTION event\n- section is not active: %s", - // JSON.stringify(section) - // ); + + //console.log("OnShowSection (card) - SHOW_SECTION event\n- section is not active: %s", + // JSON.stringify(section) + //); + } } @@ -705,9 +717,9 @@ export class Card extends LitElement { // JSON.stringify(Store.selectedConfigArea), //); - // console.log("setConfig (card) - updated configuration:\n%s", - // JSON.stringify(this.config,null,2), - // ); + //console.log("setConfig (card) - updated configuration:\n%s", + // JSON.stringify(this.config,null,2), + //); } @@ -733,6 +745,9 @@ export class Card extends LitElement { // initialize what configarea to display on entry - always GENERAL, since this is a static method. Store.selectedConfigArea = ConfigArea.GENERAL; + // clear medialist cache items. + Card.mediaListCache = {}; + // get the card configuration editor, and return for display. return document.createElement('stpc-editor'); } diff --git a/src/components/media-browser-icons.ts b/src/components/media-browser-icons.ts index 9ba878c..8f70912 100644 --- a/src/components/media-browser-icons.ts +++ b/src/components/media-browser-icons.ts @@ -31,7 +31,10 @@ export class MediaBrowserIcons extends LitElement { */ constructor() { + // invoke base class method. super(); + + // initialize storage. this.mousedownTimestamp = 0; } diff --git a/src/components/media-browser-list.ts b/src/components/media-browser-list.ts index 3696213..f099471 100644 --- a/src/components/media-browser-list.ts +++ b/src/components/media-browser-list.ts @@ -32,7 +32,10 @@ export class MediaBrowserList extends LitElement { */ constructor() { + // invoke base class method. super(); + + // initialize storage. this.mousedownTimestamp = 0; } diff --git a/src/constants.ts b/src/constants.ts index b31f867..0edf7a5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,7 @@ import { css } from 'lit'; /** current version of the card. */ -export const CARD_VERSION = '1.0.5'; +export const CARD_VERSION = '1.0.6'; /** SoundTouchPlus integration domain identifier. */ export const DOMAIN_SOUNDTOUCHPLUS = 'soundtouchplus'; @@ -21,6 +21,9 @@ export const PROGRESS_DONE = dispatchPrefix + 'progress-done'; /** uniquely identifies the section selected event. */ export const SECTION_SELECTED = dispatchPrefix + 'section-selected'; +/** uniquely identifies the configuration updated event. */ +export const CONFIG_UPDATED = dispatchPrefix + 'config-updated'; + /** uniquely identifies the section selected event. */ export const PANDORA_BROWSER_REFRESH = dispatchPrefix + 'pandora-browser-refresh'; diff --git a/src/editor/base-editor.ts b/src/editor/base-editor.ts index aac8abf..9cfd60b 100644 --- a/src/editor/base-editor.ts +++ b/src/editor/base-editor.ts @@ -1,5 +1,5 @@ // lovelace card imports. -import { css, LitElement, PropertyValues } from 'lit'; +import { css, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { fireEvent, HomeAssistant } from 'custom-card-helpers'; @@ -10,8 +10,8 @@ import { MediaPlayer } from '../model/media-player'; import { Section } from '../types/section'; import { SourceList } from '../types/soundtouchplus/sourcelist' import { SoundTouchPlusService } from '../services/soundtouchplus-service'; -import { dispatch, getSectionForConfigArea } from '../utils/utils'; -import { SECTION_SELECTED } from '../constants'; +import { dispatch, getObjectDifferences, getSectionForConfigArea } from '../utils/utils'; +import { CONFIG_UPDATED, SECTION_SELECTED } from '../constants'; export abstract class BaseEditor extends LitElement { @@ -36,6 +36,7 @@ export abstract class BaseEditor extends LitElement { */ constructor() { + // invoke base class method. super(); } @@ -62,61 +63,6 @@ export abstract class BaseEditor extends LitElement { } - /** - * Called when an update was triggered, before rendering. Receives a Map of changed - * properties, and their previous values. This can be used for modifying or setting - * new properties before a render occurs. - */ - protected update(changedProperties: PropertyValues) { - - // invoke base class method. - super.update(changedProperties); - - // console.log("update (base-editor) - update event (pre-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); - } - - - /** - * Called when an update was triggered, after rendering. Receives a Map of changed - * properties, and their previous values. This can be used for observing and acting - * on property changes. - */ - protected updated(changedProperties: PropertyValues) { - - // invoke base class method. - super.updated(changedProperties); - - // console.log("updated (base-editor) - update event (post-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); - } - - - /** - * Called when your element has rendered for the first time. Called once in the - * lifetime of an element. Useful for one-time setup work that requires access to - * the DOM. - */ - protected firstUpdated(_changedProperties: PropertyValues): void { - - // invoke base class method. - super.firstUpdated(_changedProperties); - - // note that this will method will fire multiple times, once for each - - // console.log("firstUpdated (base-editor) - first render complete\n- this.section=%s\n- Store.selectedConfigArea=%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // ); - } - - /** * Home Assistant will call setConfig(config) when the configuration changes. This * is most likely to occur when changing the configuration via the UI editor, but @@ -162,10 +108,10 @@ export abstract class BaseEditor extends LitElement { // JSON.stringify(this.config, null, 2), // prettyprint //); - // console.log("setConfig (base-editor) exit\n- this.section=%s\n- Store.selectedConfigArea=%s", - // JSON.stringify(this.section), - // JSON.stringify(Store.selectedConfigArea), - // ); + //console.log("setConfig (base-editor) exit\n- this.section=%s\n- Store.selectedConfigArea=%s", + // JSON.stringify(this.section), + // JSON.stringify(Store.selectedConfigArea), + //); } @@ -214,13 +160,25 @@ export abstract class BaseEditor extends LitElement { */ protected configChanged(changedConfig: CardConfig | undefined = undefined) { - //console.log("configChanged (base-editor) - configuration editor value changed\n- this.section=%s\n- Store.selectedConfigArea=%s", + //console.log("configChanged (base-editor) - configuration settings changed\n- this.section=%s\n- Store.selectedConfigArea=%s", + // JSON.stringify(this.section), + // JSON.stringify(Store.selectedConfigArea), + //); + + //console.log("configChanged (base-editor) - configuration settings changed\n- this.section=%s\n- Store.selectedConfigArea=%s\n- Values changed:\n%s", // JSON.stringify(this.section), // JSON.stringify(Store.selectedConfigArea), + // JSON.stringify(getObjectDifferences(this.config, changedConfig), null, 2), //); - // update the existing configuration if configuration changes were supplied. + // were configuration changes supplied? + let changedValues = {} if (changedConfig) { + + // get configuration changes. + changedValues = getObjectDifferences(this.config, changedConfig); + + // update the existing configuration with supplied changes. this.config = { ...this.config, ...changedConfig, @@ -240,22 +198,27 @@ export abstract class BaseEditor extends LitElement { dispatch(SECTION_SELECTED, this.section); } - //console.log("configChanged (base-editor) - configuration editor value changed\n- changedConfig:\n%s", + //console.log("configChanged (base-editor) - configuration settings changed\n- changedConfig:\n%s", // JSON.stringify(changedConfig,null,2) //); - // fire an event indicating that the configuration has changed. + // inform Home assistant dashboard that our configuration has changed. fireEvent(this, 'config-changed', { config: this.config }); // request an update, which will force the card editor to re-render. this.requestUpdate(); - // configAreaSection = getSectionForConfigArea(Store.selectedConfigArea); - // console.log("configChanged (base-editor) - after requestUpdate\n- this.section=%s\n- configAreaSection=%s\n- Store.selectedConfigArea=%s", - // JSON.stringify(this.section), - // JSON.stringify(configAreaSection), - // JSON.stringify(Store.selectedConfigArea), - // ); + // inform configured component of the changes; we will let them decide whether to + // re-render the component, refresh media lists, etc. + dispatch(CONFIG_UPDATED, changedValues); + + //const configAreaSection2 = getSectionForConfigArea(Store.selectedConfigArea); + //console.log("configChanged (base-editor) - after requestUpdate\n- this.section=%s\n- Store.selectedConfigArea=%s", + // JSON.stringify(this.section), + // JSON.stringify(configAreaSection2), + // JSON.stringify(Store.selectedConfigArea), + //); + } @@ -301,7 +264,7 @@ export abstract class BaseEditor extends LitElement { } } } else { - // console.log("getSourceAccountsList (base-editor) - player reference not set!"); + //console.log("getSourceAccountsList (base-editor) - player reference not set!"); } // if no sources found, then add a dummy entry. diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 04a77fd..a7ade83 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -23,20 +23,10 @@ import { getSectionForConfigArea, } from '../utils/utils'; -/** Configuration area editor section keys array. */ -const { - GENERAL, - PLAYER, - SOURCE_BROWSER, - RECENT_BROWSER, - USERPRESET_BROWSER, - PRESET_BROWSER, - PANDORA_BROWSER -} = ConfigArea; class CardEditor extends BaseEditor { - @state() private configArea = GENERAL; + @state() private configArea = ConfigArea.GENERAL; /** * Invoked on each update to perform rendering tasks. @@ -61,7 +51,7 @@ class CardEditor extends BaseEditor { return html` - ${[GENERAL, PLAYER, SOURCE_BROWSER, PANDORA_BROWSER].map( + ${[ConfigArea.GENERAL, ConfigArea.PLAYER, ConfigArea.SOURCE_BROWSER, ConfigArea.PANDORA_BROWSER].map( (configArea) => html` - ${[PRESET_BROWSER, USERPRESET_BROWSER, RECENT_BROWSER].map( + ${[ConfigArea.PRESET_BROWSER, ConfigArea.USERPRESET_BROWSER, ConfigArea.RECENT_BROWSER].map( (configArea) => html` html``, ], [ - PANDORA_BROWSER, + ConfigArea.PANDORA_BROWSER, () => html``, ], [ - PLAYER, + ConfigArea.PLAYER, () => html``, ], [ - PRESET_BROWSER, + ConfigArea.PRESET_BROWSER, () => html``, ], [ - RECENT_BROWSER, + ConfigArea.RECENT_BROWSER, () => html``, ], [ - SOURCE_BROWSER, + ConfigArea.SOURCE_BROWSER, () => html``, ], [ - USERPRESET_BROWSER, + ConfigArea.USERPRESET_BROWSER, () => html``, ], ]); @@ -157,11 +147,12 @@ class CardEditor extends BaseEditor { // show the section that we are editing. const sectionNew = getSectionForConfigArea(configArea); - //console.log("OnConfigSectionClick (editor)\n- OLD configArea=%s\n- NEW configArea=%s\n- OLD section=%s\n- NEW section=%s", + //console.log("OnConfigSectionClick (editor)\n- OLD configArea=%s\n- NEW configArea=%s\n- OLD section=%s\n- NEW section=%s\n- Store.selectedConfigArea=%s", // JSON.stringify(this.configArea), // JSON.stringify(configArea), // JSON.stringify(this.section), - // JSON.stringify(sectionNew) + // JSON.stringify(sectionNew), + // JSON.stringify(Store.selectedConfigArea), //); // store selected ConfigArea. @@ -218,51 +209,15 @@ class CardEditor extends BaseEditor { } - /** - * Called when an update was triggered, before rendering. Receives a Map of changed - * properties, and their previous values. This can be used for modifying or setting - * new properties before a render occurs. - */ - protected update(changedProperties: PropertyValues) { - - // invoke base class method. - super.update(changedProperties); - - // console.log("update (editor) - update event (pre-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); - } - - - /** - * Called when an update was triggered, after rendering. Receives a Map of changed - * properties, and their previous values. This can be used for observing and acting - * on property changes. - */ - protected updated(changedProperties: PropertyValues) { - - // invoke base class method. - super.updated(changedProperties); - - // console.log("updated (editor) - update event (post-render)\n- this.section=%s\n- Store.selectedConfigArea=%s\nChanged Property Keys:\n%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // JSON.stringify(changedProperties.keys()), - // ); - } - - /** * Called when your element has rendered for the first time. Called once in the * lifetime of an element. Useful for one-time setup work that requires access to * the DOM. */ - protected firstUpdated(_changedProperties: PropertyValues): void { + protected firstUpdated(changedProperties: PropertyValues): void { // invoke base class method. - super.firstUpdated(_changedProperties); + super.firstUpdated(changedProperties); // if there are things that you only want to happen one time when the configuration // is initially loaded, then do them here. @@ -278,10 +233,10 @@ class CardEditor extends BaseEditor { Store.selectedConfigArea = this.configArea; this.requestUpdate(); - // console.log("firstUpdated (editor) - first render complete\n- this.section=%s\n- Store.selectedConfigArea=%s", - // JSON.stringify(this.section || '*undefined*'), - // JSON.stringify(Store.selectedConfigArea), - // ); + //console.log("firstUpdated (editor) - first render complete\n- this.section=%s\n- Store.selectedConfigArea=%s", + // JSON.stringify(this.section || '*undefined*'), + // JSON.stringify(Store.selectedConfigArea), + //); } diff --git a/src/editor/general-editor.ts b/src/editor/general-editor.ts index 8e9f494..5272139 100644 --- a/src/editor/general-editor.ts +++ b/src/editor/general-editor.ts @@ -15,6 +15,7 @@ const CONFIG_SETTINGS_SCHEMA = [ required: false, type: 'multi_select', options: { + /* the following must match defined names in `secion.ts` */ player: 'Player', sources: 'Sources', presets: 'Device Presets', diff --git a/src/sections/pandora-browser.ts b/src/sections/pandora-browser.ts index 09bdcbd..0418507 100644 --- a/src/sections/pandora-browser.ts +++ b/src/sections/pandora-browser.ts @@ -6,10 +6,11 @@ import { HomeAssistant } from 'custom-card-helpers'; // our imports. import '../components/media-browser-list'; import '../components/media-browser-icons'; +import { Card } from '../card'; import { Store } from '../model/store'; -import { MediaPlayer } from '../model/media-player'; -import { customEvent } from '../utils/utils'; +import { customEvent, isCardInEditPreview } from '../utils/utils'; import { formatTitleInfo } from '../utils/media-browser-utils'; +import { MediaPlayer } from '../model/media-player'; import { ITEM_SELECTED, PANDORA_BROWSER_REFRESH, SECTION_SELECTED } from '../constants'; import { SoundTouchPlusService } from '../services/soundtouchplus-service'; import { CardConfig } from '../types/cardconfig' @@ -38,7 +39,7 @@ export class PandoraBrowser extends LitElement { private mediaListLastUpdatedOn!: number; /** SoundTouchPlus device navigate response list. */ - private mediaList!: NavigateResponse; + private mediaList!: NavigateResponse | undefined; /** SoundTouchPlus services instance. */ private soundTouchPlusService!: SoundTouchPlusService; @@ -49,10 +50,10 @@ export class PandoraBrowser extends LitElement { */ constructor() { - // initialize storage. + // invoke base class method. super(); - // force refresh first time. + // initialize storage. this.isUpdateInProgress = false; this.mediaListLastUpdatedOn = -1; } @@ -66,7 +67,9 @@ export class PandoraBrowser extends LitElement { */ protected render(): TemplateResult | void { - //console.log("pandora-browser.render()\n Rendering pandora browser html"); + //console.log("render (pandora-browser) - rendering control\n- mediaListLastUpdatedOn=%s", + // JSON.stringify(this.mediaListLastUpdatedOn) + //); // set common references from application common storage area. this.hass = this.store.hass @@ -78,7 +81,7 @@ export class PandoraBrowser extends LitElement { if (!this.player) throw new Error("SoundTouchPlus media player entity id not configured"); - // is this the first render? if so, then refresh the list. + // does the medialist need refreshing? if (this.mediaListLastUpdatedOn == -1) { if (!this.isUpdateInProgress) { this.isUpdateInProgress = true; @@ -88,9 +91,6 @@ export class PandoraBrowser extends LitElement { } } - //console.log(LOGPFX + "render()\n NavigateResponse.LastUpdatedOn=%s", this.mediaList ? this.mediaList.LastUpdatedOn : "unknown"); - //console.log(LOGPFX + "render()\n this.mediaList='%s'", JSON.stringify(this.mediaList)); - // format title and sub-title details. const title = formatTitleInfo(this.config.pandoraBrowserTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Items); const subtitle = formatTitleInfo(this.config.pandoraBrowserSubTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Items); @@ -256,10 +256,15 @@ export class PandoraBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelected = (args: CustomEvent) => { - //console.log("pandora-browser.OnItemSelected - args:\n%s", JSON.stringify(args)); + + //console.log("OnItemSelected (pandora-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; this.PlayItem(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED, mediaItem)); + }; @@ -270,6 +275,10 @@ export class PandoraBrowser extends LitElement { */ private async PlayItem(mediaItem: NavigateItem) { + //console.log("PlayItem (pandora-browser) - media item:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); + if (mediaItem.ContentItem) { // play the content. @@ -300,14 +309,49 @@ export class PandoraBrowser extends LitElement { return; } + // if card is being edited, then we will use the cached media list as the data source; + // otherwise, we will refresh the media list from the real-time source. + const cacheKey = 'pandora-browser'; + this.mediaList = undefined; + const isCardEditMode = isCardInEditPreview(this.store.card); + if ((isCardEditMode) && (cacheKey in Card.mediaListCache)) { + + //console.log("%c updateMediaList (pandora-browser) - medialist loaded from cache", + // "color: orange;", + //); + + this.mediaList = Card.mediaListCache[cacheKey] as NavigateResponse; + this.isUpdateInProgress = false; + this.requestUpdate(); + return; + } + + //console.log("%c updateMediaList (pandora-browser) - updating medialist", + // "color: orange;", + //); + // call the service to retrieve the media list. this.soundTouchPlusService.MusicServiceStationList(player.id, "PANDORA", this.config.pandoraSourceAccount, "stationName") .then(result => { + this.mediaList = result; this.mediaListLastUpdatedOn = result.LastUpdatedOn || (Date.now() / 1000); this.isUpdateInProgress = false; - //console.log("%c pandora-browser render - updateMediaList check info AFTER update:\n %s=mediaListLastUpdatedOn", "color: green;", JSON.stringify(this.mediaListLastUpdatedOn)); this.requestUpdate(); + + //console.log("%c pandora-browser - updateMediaList info AFTER update:\n- player id = %s\n- %s = mediaListLastUpdatedOn", + // "color: green;", + // this.player.id, + // this.mediaListLastUpdatedOn); + + // if editing the card then store the list in the cache for next time. + if ((isCardEditMode) && !(cacheKey in Card.mediaListCache)) { + Card.mediaListCache[cacheKey] = this.mediaList; + //console.log("%c updateMediaList (pandora-browser) - medialist stored to cache", + // "color: orange;", + //); + } + }); } } \ No newline at end of file diff --git a/src/sections/preset-browser.ts b/src/sections/preset-browser.ts index 4da127f..bfc404d 100644 --- a/src/sections/preset-browser.ts +++ b/src/sections/preset-browser.ts @@ -6,8 +6,9 @@ import { HomeAssistant } from 'custom-card-helpers'; // our imports. import '../components/media-browser-list'; import '../components/media-browser-icons'; +import { Card } from '../card'; import { Store } from '../model/store'; -import { customEvent } from '../utils/utils'; +import { customEvent, isCardInEditPreview } from '../utils/utils'; import { formatTitleInfo } from '../utils/media-browser-utils'; import { MediaPlayer } from '../model/media-player'; import { ITEM_SELECTED, ITEM_SELECTED_WITH_HOLD, SECTION_SELECTED } from '../constants'; @@ -40,7 +41,7 @@ export class PresetBrowser extends LitElement { private mediaListLastUpdatedOn!: number; /** SoundTouchPlus device preset list. */ - private mediaList!: PresetList; + private mediaList!: PresetList | undefined; /** SoundTouchPlus services instance. */ private soundTouchPlusService!: SoundTouchPlusService; @@ -51,10 +52,10 @@ export class PresetBrowser extends LitElement { */ constructor() { - // initialize storage. + // invoke base class method. super(); - // force refresh first time. + // initialize storage. this.isUpdateInProgress = false; this.mediaListLastUpdatedOn = -1; } @@ -70,7 +71,9 @@ export class PresetBrowser extends LitElement { */ protected render(): TemplateResult | void { - //console.log(LOGPFX + "render()\n Rendering preset browser html"); + //console.log("render (preset-browser) - rendering control\n- mediaListLastUpdatedOn=%s", + // JSON.stringify(this.mediaListLastUpdatedOn) + //); // set common references from application common storage area. this.hass = this.store.hass @@ -82,14 +85,8 @@ export class PresetBrowser extends LitElement { if (!this.player) throw new Error("SoundTouchPlus media player entity id not configured"); - // was the media player preset list updated? + // does the medialist need refreshing? const playerLastUpdatedOn = (this.player.attributes.soundtouchplus_presets_lastupdated || 0); - //console.log("%c preset-browser - updateMediaList info BEFORE update:\n player id=%s\n %s=player.stp_presets_lastupdated\n %s=playerLastUpdatedOn\n %s=mediaListLastUpdatedOn", - // "color: green;", - // this.player.id, - // this.player.attributes.soundtouchplus_presets_lastupdated, - // playerLastUpdatedOn, - // this.mediaListLastUpdatedOn); if ((this.mediaListLastUpdatedOn == -1) || (playerLastUpdatedOn > this.mediaListLastUpdatedOn)) { if (!this.isUpdateInProgress) { this.isUpdateInProgress = true; @@ -99,9 +96,6 @@ export class PresetBrowser extends LitElement { } } - //console.log(LOGPFX + "render()\n PresetList.LastUpdatedOn=%s", this.mediaList ? this.mediaList.LastUpdatedOn : "unknown"); - //console.log(LOGPFX + "render()\n this.mediaList='%s'", JSON.stringify(this.mediaList)); - // format title and sub-title details. const title = formatTitleInfo(this.config.presetBrowserTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Presets); const subtitle = formatTitleInfo(this.config.presetBrowserSubTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Presets); @@ -212,9 +206,15 @@ export class PresetBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelected = (args: CustomEvent) => { + + //console.log("OnItemSelected (preset-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; this.SelectPreset(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED, mediaItem)); + }; @@ -226,9 +226,15 @@ export class PresetBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelectedWithHold = (args: CustomEvent) => { + + //console.log("OnItemSelectedWithHold (preset-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; this.StorePreset(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED_WITH_HOLD, mediaItem)); + }; @@ -239,6 +245,10 @@ export class PresetBrowser extends LitElement { */ private async SelectPreset(mediaItem: Preset) { + //console.log("SelectPreset (pandora-browser) - media item:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); + if (mediaItem.PresetId) { // play the content. @@ -257,9 +267,15 @@ export class PresetBrowser extends LitElement { * @param mediaItem The Preset item that was selected. */ private async StorePreset(mediaItem: Preset) { + + //console.log("StorePreset (pandora-browser) - media item:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); + if (mediaItem.PresetId) { await this.soundTouchPlusService.RemoteKeyPress(this.player.id, "PRESET_" + JSON.stringify(mediaItem.PresetId), "press"); } + } @@ -279,20 +295,52 @@ export class PresetBrowser extends LitElement { // refresh is only triggered once on the attribute change (or initial request). this.mediaListLastUpdatedOn = player.attributes.soundtouchplus_presets_lastupdated || (Date.now() / 1000); + // if card is being edited, then we will use the cached media list as the data source; + // otherwise, we will refresh the media list from the real-time source. + const cacheKey = 'preset-browser'; + this.mediaList = undefined; + const isCardEditMode = isCardInEditPreview(this.store.card); + if ((isCardEditMode) && (cacheKey in Card.mediaListCache)) { + + //console.log("%c updateMediaList (preset-browser) - medialist loaded from cache", + // "color: orange;", + //); + + this.mediaList = Card.mediaListCache[cacheKey] as PresetList; + this.isUpdateInProgress = false; + this.requestUpdate(); + return; + } + + //console.log("%c updateMediaList (preset-browser) - updating medialist", + // "color: orange;", + //); + // call the service to retrieve the media list. this.soundTouchPlusService.PresetList(player.id, true) .then(result => { + this.mediaList = result; this.mediaListLastUpdatedOn = result.LastUpdatedOn || (Date.now() / 1000); this.isUpdateInProgress = false; + this.requestUpdate(); + //const playerLastUpdatedOn = (this.player.attributes.soundtouchplus_presets_lastupdated || 0); - //console.log("%c preset-browser - updateMediaList info AFTER update:\n player id=%s\n %s=player.stp_presets_lastupdated\n %s=playerLastUpdatedOn\n %s=mediaListLastUpdatedOn", + //console.log("%c preset-browser - updateMediaList info AFTER update:\n- player id = %s\n- %s = player.soundtouchplus_presets_lastupdated\n- %s = playerLastUpdatedOn\n- %s = mediaListLastUpdatedOn", // "color: green;", // this.player.id, // this.player.attributes.soundtouchplus_presets_lastupdated, // playerLastUpdatedOn, // this.mediaListLastUpdatedOn); - this.requestUpdate(); + + // if editing the card then store the list in the cache for next time. + if ((isCardEditMode) && !(cacheKey in Card.mediaListCache)) { + Card.mediaListCache[cacheKey] = this.mediaList; + //console.log("%c updateMediaList (preset-browser) - medialist stored to cache", + // "color: orange;", + //); + } + }); } } \ No newline at end of file diff --git a/src/sections/recent-browser.ts b/src/sections/recent-browser.ts index c57cfa7..a715f75 100644 --- a/src/sections/recent-browser.ts +++ b/src/sections/recent-browser.ts @@ -6,8 +6,9 @@ import { HomeAssistant } from 'custom-card-helpers'; // our imports. import '../components/media-browser-list'; import '../components/media-browser-icons'; +import { Card } from '../card'; import { Store } from '../model/store'; -import { customEvent } from '../utils/utils'; +import { customEvent, isCardInEditPreview } from '../utils/utils'; import { formatTitleInfo } from '../utils/media-browser-utils'; import { MediaPlayer } from '../model/media-player'; import { ITEM_SELECTED, SECTION_SELECTED } from '../constants'; @@ -40,7 +41,7 @@ export class RecentBrowser extends LitElement { private mediaListLastUpdatedOn!: number; /** SoundTouchPlus device recent list. */ - private mediaList!: RecentList; + private mediaList!: RecentList | undefined; /** SoundTouchPlus services instance. */ private soundTouchPlusService!: SoundTouchPlusService; @@ -51,10 +52,10 @@ export class RecentBrowser extends LitElement { */ constructor() { - // initialize storage. + // invoke base class method. super(); - // force refresh first time. + // initialize storage. this.isUpdateInProgress = false; this.mediaListLastUpdatedOn = -1; } @@ -67,7 +68,9 @@ export class RecentBrowser extends LitElement { */ protected render(): TemplateResult | void { - //console.log(LOGPFX + "render()\n Rendering recent browser html"); + //console.log("render (recent-browser) - rendering control\n- mediaListLastUpdatedOn=%s", + // JSON.stringify(this.mediaListLastUpdatedOn) + //); // set common references from application common storage area. this.hass = this.store.hass @@ -81,12 +84,6 @@ export class RecentBrowser extends LitElement { // was the media player recent list cache updated? const playerLastUpdatedOn = (this.player.attributes.soundtouchplus_recents_cache_lastupdated || 0); - //console.log("%c recent-browser - updateMediaList info BEFORE update:\n player id=%s\n %s=player.stp_recents_cache_lastupdated\n %s=playerLastUpdatedOn\n %s=mediaListLastUpdatedOn", - // "color: green;", - // this.player.id, - // this.player.attributes.soundtouchplus_recents_cache_lastupdated, - // playerLastUpdatedOn, - // this.mediaListLastUpdatedOn); if ((this.mediaListLastUpdatedOn == -1) || (playerLastUpdatedOn > this.mediaListLastUpdatedOn)) { if (!this.isUpdateInProgress) { this.isUpdateInProgress = true; @@ -96,9 +93,6 @@ export class RecentBrowser extends LitElement { } } - //console.log(LOGPFX + "render()\n RecentList.LastUpdatedOn=%s", this.mediaList ? this.mediaList.LastUpdatedOn : "unknown"); - //console.log(LOGPFX + "render()\n this.mediaList='%s'", JSON.stringify(this.mediaList)); - // format title and sub-title details. const title = formatTitleInfo(this.config.recentBrowserTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Recents); const subtitle = formatTitleInfo(this.config.recentBrowserSubTitle, this.config, this.player, this.mediaListLastUpdatedOn, this.mediaList?.Recents); @@ -200,9 +194,15 @@ export class RecentBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelected = (args: CustomEvent) => { + + //console.log("OnItemSelected (recent-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; this.PlayItem(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED, mediaItem)); + }; @@ -213,6 +213,10 @@ export class RecentBrowser extends LitElement { */ private async PlayItem(mediaItem: Recent) { + //console.log("PlayItem (recent-browser) - media item:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); + if (mediaItem.ContentItem) { // play the content. @@ -239,20 +243,52 @@ export class RecentBrowser extends LitElement { // refresh is only triggered once on the attribute change (or initial request). this.mediaListLastUpdatedOn = player.attributes.soundtouchplus_recents_cache_lastupdated || (Date.now() / 1000); + // if card is being edited, then we will use the cached media list as the data source; + // otherwise, we will refresh the media list from the real-time source. + const cacheKey = 'recent-browser'; + this.mediaList = undefined; + const isCardEditMode = isCardInEditPreview(this.store.card); + if ((isCardEditMode) && (cacheKey in Card.mediaListCache)) { + + //console.log("%c updateMediaList (recent-browser) - medialist loaded from cache", + // "color: orange;", + //); + + this.mediaList = Card.mediaListCache[cacheKey] as RecentList; + this.isUpdateInProgress = false; + this.requestUpdate(); + return; + } + + //console.log("%c updateMediaList (recent-browser) - updating medialist", + // "color: orange;", + //); + // call the service to retrieve the media list. this.soundTouchPlusService.RecentListCache(player.id) .then(result => { + this.mediaList = result; this.mediaListLastUpdatedOn = result.LastUpdatedOn || (Date.now() / 1000); this.isUpdateInProgress = false; + this.requestUpdate(); + //const playerLastUpdatedOn = (this.player.attributes.soundtouchplus_recents_cache_lastupdated || 0); - //console.log("%c recent-browser - updateMediaList info AFTER update:\n player id=%s\n %s=player.stp_recents_cache_lastupdated\n %s=playerLastUpdatedOn\n %s=mediaListLastUpdatedOn", + //console.log("%c recent-browser - updateMediaList info AFTER update:\n- player id = %s\n- %s = player.soundtouchplus_recents_cache_lastupdated\n- %s = playerLastUpdatedOn\n- %s = mediaListLastUpdatedOn", // "color: green;", // this.player.id, // this.player.attributes.soundtouchplus_recents_cache_lastupdated, // playerLastUpdatedOn, // this.mediaListLastUpdatedOn); - this.requestUpdate(); + + // if editing the card then store the list in the cache for next time. + if ((isCardEditMode) && !(cacheKey in Card.mediaListCache)) { + Card.mediaListCache[cacheKey] = this.mediaList; + //console.log("%c updateMediaList (recent-browser) - medialist stored to cache", + // "color: orange;", + //); + } + }); } } \ No newline at end of file diff --git a/src/sections/source-browser.ts b/src/sections/source-browser.ts index 29e378c..72deb9f 100644 --- a/src/sections/source-browser.ts +++ b/src/sections/source-browser.ts @@ -18,14 +18,15 @@ import { // our imports. import '../components/media-browser-list'; import '../components/media-browser-icons'; +import { Card } from '../card'; import { Store } from '../model/store'; -import { MediaPlayer } from '../model/media-player'; -import { customEvent } from '../utils/utils'; +import { customEvent, isCardInEditPreview } from '../utils/utils'; import { formatTitleInfo, getMdiIconImageUrl } from '../utils/media-browser-utils'; +import { MediaPlayer } from '../model/media-player'; import { ITEM_SELECTED, SECTION_SELECTED } from '../constants'; import { CardConfig } from '../types/cardconfig' -import { Section } from '../types/section' import { ContentItemParent, ContentItem } from '../types/soundtouchplus/contentitem'; +import { Section } from '../types/section' export class SourceBrowser extends LitElement { @@ -49,7 +50,7 @@ export class SourceBrowser extends LitElement { private mediaListLastUpdatedOn!: number; /** Media player source list. */ - private mediaList!: ContentItemParent[]; + private mediaList!: ContentItemParent[] | undefined; /** @@ -57,10 +58,10 @@ export class SourceBrowser extends LitElement { */ constructor() { - // initialize storage. + // invoke base class method. super(); - // force refresh first time. + // initialize storage. this.isUpdateInProgress = false; this.mediaListLastUpdatedOn = -1; } @@ -74,6 +75,10 @@ export class SourceBrowser extends LitElement { */ protected render(): TemplateResult | void { + //console.log("render (source-browser) - rendering control\n- mediaListLastUpdatedOn=%s", + // JSON.stringify(this.mediaListLastUpdatedOn) + //); + // set common references from application common storage area. this.hass = this.store.hass this.config = this.store.config; @@ -83,7 +88,7 @@ export class SourceBrowser extends LitElement { if (!this.player) throw new Error("SoundTouchPlus media player entity id not configured"); - // is this the first render? if so, then refresh the list. + // does the medialist need refreshing? if (this.mediaListLastUpdatedOn == -1) { if (!this.isUpdateInProgress) { this.isUpdateInProgress = true; @@ -201,10 +206,15 @@ export class SourceBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelected = (args: CustomEvent) => { - //console.log("source-browser.OnItemSelected - args:\n%s \nargs.detail:\n%s", JSON.stringify(args), JSON.stringify(args.detail)); + + //console.log("OnItemSelected (source-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; // a ContentItemParent object this.SelectSource(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED, mediaItem)); + }; @@ -215,7 +225,9 @@ export class SourceBrowser extends LitElement { */ private async SelectSource(mediaItem: ContentItemParent) { - //console.log("source-browser.SelectSource - mediaItem:\n%s", JSON.stringify(mediaItem)); + //console.log("SelectSource (source-browser) - select source\n- mediaItem:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); // call service to select the source. await this.store.mediaControlService.sourceSelect(this.player, mediaItem.ContentItem?.Name || ''); @@ -238,9 +250,31 @@ export class SourceBrowser extends LitElement { // with the current epoch date (in seconds) so that the refresh is only triggered once. this.mediaListLastUpdatedOn = (Date.now() / 1000); + // if card is being edited, then we will use the cached media list as the data source; + // otherwise, we will refresh the media list from the real-time source. + const cacheKey = 'source-browser'; + this.mediaList = undefined; + const isCardEditMode = isCardInEditPreview(this.store.card); + if ((isCardEditMode) && (cacheKey in Card.mediaListCache)) { + + //console.log("%c updateMediaList (source-browser) - medialist loaded from cache", + // "color: orange;", + //); + + this.mediaList = Card.mediaListCache[cacheKey] as Array; + this.isUpdateInProgress = false; + this.requestUpdate(); + return; + } + // no need to call a service - just use the source_list attribute for the list. // we could have simplified this, but wanted to keep the structure like the other sections. + //console.log("%c updateMediaList (source-browser) - updating medialist\n-source_list:\n%s", + // "color: orange;", + // JSON.stringify(player.attributes.source_list,null,2) + //); + // build an array of ContentItemParent objects that can be used in the media browser. this.mediaList = new Array(); for (const source of (player.attributes.source_list || [])) { @@ -277,12 +311,22 @@ export class SourceBrowser extends LitElement { this.mediaListLastUpdatedOn = (Date.now() / 1000); this.isUpdateInProgress = false; + this.requestUpdate(); + //console.log("%c source-browser - updateMediaList info AFTER update:\n player id=%s\n %s=mediaListLastUpdatedOn\n source_list:\n%s\n mediaList:\n%s", // "color: green;", // this.player.id, // this.mediaListLastUpdatedOn, // this.player.attributes.source_list, - // JSON.stringify(this.mediaList)); - this.requestUpdate(); + // JSON.stringify(this.mediaList) + //); + + // if editing the card then store the list in the cache for next time. + if ((isCardEditMode) && !(cacheKey in Card.mediaListCache)) { + Card.mediaListCache[cacheKey] = this.mediaList; + //console.log("%c updateMediaList (source-browser) - medialist stored to cache", + // "color: orange;", + //); + } } } \ No newline at end of file diff --git a/src/sections/userpreset-browser.ts b/src/sections/userpreset-browser.ts index 86199e4..e62aaea 100644 --- a/src/sections/userpreset-browser.ts +++ b/src/sections/userpreset-browser.ts @@ -9,8 +9,9 @@ import { HomeAssistant } from 'custom-card-helpers'; // our imports. import '../components/media-browser-list'; import '../components/media-browser-icons'; +import { Card } from '../card'; import { Store } from '../model/store'; -import { customEvent } from '../utils/utils'; +import { customEvent, isCardInEditPreview } from '../utils/utils'; import { formatTitleInfo } from '../utils/media-browser-utils'; import { MediaPlayer } from '../model/media-player'; import { ITEM_SELECTED, PROGRESS_DONE, PROGRESS_STARTED, SECTION_SELECTED } from '../constants'; @@ -40,7 +41,7 @@ export class UserPresetBrowser extends LitElement { private mediaListLastUpdatedOn!: number; /** User preset list. */ - private mediaList!: ContentItemParent[]; + private mediaList!: ContentItemParent[] | undefined; /** SoundTouchPlus services instance. */ private soundTouchPlusService!: SoundTouchPlusService; @@ -51,10 +52,10 @@ export class UserPresetBrowser extends LitElement { */ constructor() { - // initialize storage. + // invoke base class method. super(); - // force refresh first time. + // initialize storage. this.isUpdateInProgress = false; this.mediaListLastUpdatedOn = -1; } @@ -97,13 +98,6 @@ export class UserPresetBrowser extends LitElement { alertText = 'No User Presets found'; } - //console.log("userpreset-browser() - config.userPresets:\n%s", JSON.stringify(this.config.userPresets, null, 2)); - //for (const item of (this.config.userPresets || [])) { - // const cip = item as ContentItemParent; - // console.log("userPresets.ContentItem:\n%s", JSON.stringify(cip.ContentItem, null, 2)); - // console.log("userPresets.Name = %s", JSON.stringify(cip.ContentItem?.Name)); - //} - // render html. return html`
@@ -202,6 +196,11 @@ export class UserPresetBrowser extends LitElement { * @param args Event arguments that contain the media item that was clicked on. */ protected OnItemSelected = (args: CustomEvent) => { + + //console.log("OnItemSelected (userpreset-browser) - media item selected:\n%s", + // JSON.stringify(args.detail, null, 2), + //); + const mediaItem = args.detail; this.PlayItem(mediaItem); this.dispatchEvent(customEvent(ITEM_SELECTED, mediaItem)); @@ -215,6 +214,10 @@ export class UserPresetBrowser extends LitElement { */ private async PlayItem(mediaItem: ContentItemParent) { + //console.log("PlayItem (userpreset-browser) - media item:\n%s", + // JSON.stringify(mediaItem, null, 2), + //); + if (mediaItem.ContentItem) { // play the content. @@ -240,65 +243,65 @@ export class UserPresetBrowser extends LitElement { // we also copy the array (do not use `this.mediaList = this.config.userPresets`, as it // maintains object references and causes objects to keep appending!). this.mediaListLastUpdatedOn = (Date.now() / 1000); + + // if card is being edited, then we will use the cached media list as the data source; + // otherwise, we will refresh the media list from the real-time source. + const cacheKey = 'userpreset-browser'; + this.mediaList = undefined; + const isCardEditMode = isCardInEditPreview(this.store.card); + if ((isCardEditMode) && (cacheKey in Card.mediaListCache)) { + + //console.log("%c updateMediaList (userpreset-browser) - medialist loaded from cache", + // "color: orange;", + //); + + this.mediaList = Card.mediaListCache[cacheKey] as ContentItemParent[]; + this.isUpdateInProgress = false; + this.requestUpdate(); + return; + } + + //console.log("%c updateMediaList (userpreset-browser) - updating medialist", + // "color: orange;", + //); + this.mediaList = JSON.parse(JSON.stringify(this.config.userPresets || [])); + if (!this.mediaList) { + this.mediaList = new Array(); + } - //console.log('mediaList[0].ContentItem:\n%s', JSON.stringify(this.mediaList[0].ContentItem, null, 2)); - //console.log('mediaList[0].ContentItem.Name:\n%s', JSON.stringify(this.mediaList[0].ContentItem?.Name)); + //console.log("%c updateMediaList (userpreset-browser) - medialist before url load:\n%s", + // "color: orange;", + // JSON.stringify(this.mediaList,null,2), + //); // was a user presets url specified? if (this.config.userPresetsFile || '' != '') { - //// fetch the user presets from the url, and combine them with the config entries. - ////console.log('userpreset-browser.updateMediaList - userPresetsFile specified - loading content'); - //const url = this.config.userPresetsFile + '?nocache=' + Date.now() // force refresh if cached - //fetch(url) - // .then(response => response.json()) - // .then(result => { - // //console.log('mediaList BEFORE push:\n%s', JSON.stringify(this.mediaList, null, 2)); - // this.mediaList.push(...result); - // //console.log('mediaList AFTER push:\n%s', JSON.stringify(this.mediaList, null, 2)); - // this.mediaListLastUpdatedOn = (Date.now() / 1000); - // this.isUpdateInProgress = false; - // this.requestUpdate(); - // //console.log('userpreset-browser.updateMediaList - userPresetsFile specified - loading content COMPLETE 1'); - // //console.log('mediaList[1].ContentItem:\n%s', JSON.stringify(this.mediaList[1].ContentItem, null, 2)); - // //console.log('mediaList[1].ContentItem.Name:\n%s', JSON.stringify(this.mediaList[1].ContentItem?.Name)); - // }); - - //// fetch the user presets from the url, and combine them with the config entries. - //const url = this.config.userPresetsFile + '?nocache=' + Date.now() // force refresh if cached - //fetch(url) - // .then(response => { - // if (!response.ok) { - // throw new Error('Network response was not ok'); - // } - // return response.json(); - // }) - // .then(result => { - // this.mediaList.push(...result as ContentItemParent[]); - // this.mediaListLastUpdatedOn = (Date.now() / 1000); - // this.isUpdateInProgress = false; - // this.requestUpdate(); - // }) - // .catch(error => { - // console.error(`Could not fetch user presets url:`, error); - // }); - // call the service to retrieve the media list. const url = this.config.userPresetsFile + '?nocache=' + Date.now() // force refresh if cached this.UserPresetList(url) .then(result => { - //console.log('mediaList BEFORE push:\n%s', JSON.stringify(this.mediaList, null, 2)); - this.mediaList.push(...result); - //console.log('mediaList AFTER push:\n%s', JSON.stringify(this.mediaList, null, 2)); + + (this.mediaList || []).push(...result); this.mediaListLastUpdatedOn = (Date.now() / 1000); this.isUpdateInProgress = false; - //const playerLastUpdatedOn = (this.player.attributes.soundtouchplus_presets_lastupdated || 0); - //console.log("%c userpreset-browser - updateMediaList info AFTER update:\n player id=%s\n %s=mediaListLastUpdatedOn", - // "color: green;", - // this.player.id, - // this.mediaListLastUpdatedOn); this.requestUpdate(); + + //console.log("%c userpreset-browser - updateMediaList info AFTER update:\n- mediaListLastUpdatedOn=%s\n- player id=%s", + // "color: green;", + // JSON.stringify(this.mediaListLastUpdatedOn), + // JSON.stringify(this.player.id), + //); + + // if editing the card then store the list in the cache for next time. + if ((isCardEditMode) && !(cacheKey in Card.mediaListCache)) { + Card.mediaListCache[cacheKey] = this.mediaList || []; + //console.log("%c updateMediaList (userpreset-browser) - medialist stored to cache", + // "color: orange;", + //); + } + }); } else { @@ -318,13 +321,15 @@ export class UserPresetBrowser extends LitElement { try { - //console.log("%cuserpreset-browser.UserPresetList()\n Retrieving url '%s'", "color: orange;", url); + //console.log("%c UserPresetList (userpreset-browser) - Retrieving url:\n%s", + // "color: orange;", + // JSON.stringify(url), + //); // show the progress indicator on the main card. this.soundTouchPlusService.card.dispatchEvent(customEvent(PROGRESS_STARTED, { section: Section.USERPRESETS })); const responseObj = await fetch(url) - //.then(response => response.json()) .then(response => { if (!response.ok) { throw new Error('STPC - Could not fetch userpresets url (' + url + ')'); @@ -340,6 +345,11 @@ export class UserPresetBrowser extends LitElement { return []; }); + //console.log("%c updateMediaList (userpreset-browser) - medialist url response:\n%s", + // "color: orange;", + // JSON.stringify(responseObj, null, 2), + //); + return responseObj as ContentItemParent[]; } finally { diff --git a/src/services/media-control-service.ts b/src/services/media-control-service.ts index a0dfbc8..8e63e6c 100644 --- a/src/services/media-control-service.ts +++ b/src/services/media-control-service.ts @@ -202,103 +202,4 @@ export class MediaControlService { await this.volumeMute(player, muteVolume); } - - - - - //async setVolumeAndMediaForPredefinedGroup(pg: PredefinedGroup) { - // for (const pgp of pg.entities) { - // const volume = pgp.volume ?? pg.volume; - // if (volume) { - // await this.volumeSetSinglePlayer(pgp.player, volume); - // } - // if (pg.unmuteWhenGrouped) { - // await this.setVolumeMute(pgp.player, false, false); - // } - // } - // if (pg.media) { - // await this.setSource(pg.entities[0].player, pg.media); - // } - //} - - //async volumeDown(mainPlayer: MediaPlayer, updateMembers = true) { - // await this.volumeStep(mainPlayer, updateMembers, this.getStepDownVolume, 'volume_down'); - //} - - //async volumeUp(mainPlayer: MediaPlayer, updateMembers = true) { - // await this.volumeStep(mainPlayer, updateMembers, this.getStepUpVolume, 'volume_up'); - //} - - //private async volumeStep( - // mainPlayer: MediaPlayer, - // updateMembers: boolean, - // calculateVolume: (member: MediaPlayer, volumeStepSize: number) => number, - // stepDirection: string, - //) { - // if (this.config.volumeStepSize) { - // await this.volumeWithStepSize(mainPlayer, updateMembers, this.config.volumeStepSize, calculateVolume); - // } else { - // await this.volumeDefaultStep(mainPlayer, updateMembers, stepDirection); - // } - //} - - //private async volumeWithStepSize( - // mainPlayer: MediaPlayer, - // updateMembers: boolean, - // volumeStepSize: number, - // calculateVolume: CalculateVolume, - //) { - // for (const member of mainPlayer.members) { - // if (mainPlayer.id === member.id || updateMembers) { - // const newVolume = calculateVolume(member, volumeStepSize); - // await this.volumeSetSinglePlayer(member, newVolume); - // } - // } - //} - - //private getStepDownVolume(member: MediaPlayer, volumeStepSize: number) { - // return Math.max(0, member.getVolume() - volumeStepSize); - //} - - //private getStepUpVolume(member: MediaPlayer, stepSize: number) { - // return Math.min(100, member.getVolume() + stepSize); - //} - - //private async volumeDefaultStep(mainPlayer: MediaPlayer, updateMembers: boolean, stepDirection: string) { - // for (const member of mainPlayer.members) { - // if (mainPlayer.id === member.id || updateMembers) { - // if (!member.ignoreVolume) { - // await this.hassService.callMediaService(stepDirection, { entity_id: member.id }); - // } - // } - // } - //} - - //async volumeSet(player: MediaPlayer, volume: number, updateMembers: boolean) { - // if (updateMembers) { - // return await this.volumeSetGroup(player, volume); - // } else { - // return await this.volumeSetSinglePlayer(player, volume); - // } - //} - //private async volumeSetGroup(player: MediaPlayer, volumePercent: number) { - // let relativeVolumeChange: number | undefined; - // if (this.config.adjustVolumeRelativeToMainPlayer) { - // relativeVolumeChange = volumePercent / player.getVolume(); - // } - - // await Promise.all( - // player.members.map((member) => { - // let memberVolume = volumePercent; - // if (relativeVolumeChange !== undefined) { - // if (this.config.adjustVolumeRelativeToMainPlayer) { - // memberVolume = member.getVolume() * relativeVolumeChange; - // memberVolume = Math.min(100, Math.max(0, memberVolume)); - // } - // } - // return this.volumeSetSinglePlayer(member, memberVolume); - // }), - // ); - //} - } diff --git a/src/services/soundtouchplus-service.ts b/src/services/soundtouchplus-service.ts index 31d2478..3257ea5 100644 --- a/src/services/soundtouchplus-service.ts +++ b/src/services/soundtouchplus-service.ts @@ -164,6 +164,14 @@ export class SoundTouchPlusService { try { + //console.log("%c MusicServiceStationList (soundtouchplus-service)\n- entityId = %s\n- source = %s\n- sourceAccount = %s\n- sortType = %s", + // "color: orange;", + // JSON.stringify(entityId), + // JSON.stringify(source), + // JSON.stringify(sourceAccount), + // JSON.stringify(sortType), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, @@ -204,6 +212,12 @@ export class SoundTouchPlusService { if (!contentItem) throw new Error("STPC0005 contentItem argument was not supplied to the PlayContentItem service.") + //console.log("%c PlayContentItem (soundtouchplus-service)\n- entityId = %s\n- contentItem:\n%s", + // "color: orange;", + // JSON.stringify(entityId), + // JSON.stringify(contentItem,null,2), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, @@ -239,6 +253,12 @@ export class SoundTouchPlusService { try { + //console.log("%c PresetList (soundtouchplus-service)\n- entityId = %s\n- includeEmptySlots = %s", + // "color: orange;", + // JSON.stringify(entityId), + // JSON.stringify(includeEmptySlots), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, @@ -269,6 +289,11 @@ export class SoundTouchPlusService { try { + //console.log("%c RecentList (soundtouchplus-service)\n- entityId = %s", + // "color: orange;", + // JSON.stringify(entityId), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, @@ -298,6 +323,11 @@ export class SoundTouchPlusService { try { + //console.log("%c RecentListCache (soundtouchplus-service)\n- entityId = %s", + // "color: orange;", + // JSON.stringify(entityId), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, @@ -328,6 +358,13 @@ export class SoundTouchPlusService { try { + //console.log("%c RemoteKeyPress (soundtouchplus-service)\n- entityId = %s\n- keyId = %s\n- keyState = %s", + // "color: orange;", + // JSON.stringify(entityId), + // JSON.stringify(keyId), + // JSON.stringify(keyState), + //); + // create service request. const serviceRequest: ServiceCallRequest = { domain: DOMAIN_SOUNDTOUCHPLUS, diff --git a/src/types/configarea.ts b/src/types/configarea.ts index 466d571..2d6c007 100644 --- a/src/types/configarea.ts +++ b/src/types/configarea.ts @@ -3,10 +3,10 @@ */ export enum ConfigArea { GENERAL = 'General', + PANDORA_BROWSER = 'Pandora', PLAYER = 'Player', - SOURCE_BROWSER = 'Sources', - RECENT_BROWSER = 'Recently Played', PRESET_BROWSER = 'Device Presets', + RECENT_BROWSER = 'Recently Played', + SOURCE_BROWSER = 'Sources', USERPRESET_BROWSER = 'User Presets', - PANDORA_BROWSER = 'Pandora', } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 8aa941c..bf74f42 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -263,3 +263,32 @@ export function isCardInEditPreview(cardElement: Element) { export function isNumber(numStr: string): boolean { return !isNaN(parseFloat(numStr)) && !isNaN(+numStr) } + + +export function getObjectDifferences(obj1: any, obj2: any): any { + if (typeof obj1 !== 'object' || typeof obj2 !== 'object') { + return obj1 !== obj2 ? [obj1, obj2] : undefined; + } + + const keys1 = Object.keys(obj1); + const keys2 = Object.keys(obj2); + const uniqueKeys = new Set([...keys1, ...keys2]); + + const differences: any = {}; + + for (const key of uniqueKeys) { + const value1 = obj1[key]; + const value2 = obj2[key]; + + if (typeof value1 === 'object' && typeof value2 === 'object') { + const nestedDifferences = getObjectDifferences(value1, value2); + if (nestedDifferences) { + differences[key] = nestedDifferences; + } + } else if (value1 !== value2) { + differences[key] = [value1, value2]; + } + } + + return Object.keys(differences).length === 0 ? undefined : differences; +}