From 454af4b97b7ddf249002b6489c1310b9125555fd Mon Sep 17 00:00:00 2001 From: Todd Lucas Date: Mon, 20 May 2024 02:14:21 -0500 Subject: [PATCH] [ 1.0.3 ] * Updated the Player section to be a basic media player; more features are planned, but I wanted to get something out there that was functional. * Added Sources section to allow media player source to be selected. * Added recently played list cache support, that caches recently played content to the local file system. The cache list is preferred over the SoundTouch device list, since the device removes it's cover art image links quite frequently. Max cache items is configurable in the SoundTouchPlus device options (per device). * Updated wiki documentation. --- CHANGELOG.md | 11 + SoundTouchPlusCard.njsproj | 22 +- data/recently_played_cache_9070658C9D4A.xml | 33 ++ src/card.ts | 273 +++++++++------- src/components/footer.ts | 125 +++---- src/components/ha-player.ts | 13 +- src/components/media-browser-icons.ts | 81 ++--- src/components/media-browser-list.ts | 24 +- src/components/player-controls.ts | 150 +++++++++ src/components/player-header.ts | 129 ++++++++ src/components/player-progress.ts | 201 ++++++++++++ src/components/player-volume.ts | 167 ++++++++++ src/constants.ts | 5 +- src/editor/editor-form.ts | 6 + src/editor/editor.ts | 34 +- src/editor/general-editor.ts | 2 + src/editor/pandora-browser-editor.ts | 13 +- src/editor/player-controls-editor.ts | 97 ++++++ src/editor/player-editor.ts | 119 +++++++ src/editor/player-header-editor.ts | 106 ++++++ src/editor/player-volume-editor.ts | 88 +++++ src/editor/source-browser-editor.ts | 90 ++++++ src/main.ts | 6 +- src/model/media-player.ts | 53 +++ src/model/store.ts | 91 ++++++ src/sections/pandora-browser.ts | 252 ++++++++------- src/sections/player.ts | 172 ++++++---- src/sections/preset-browser.ts | 261 ++++++++------- src/sections/recent-browser.ts | 259 ++++++++------- src/sections/source-browser.ts | 288 +++++++++++++++++ src/services/hass-service.ts | 32 +- src/services/media-control-service.ts | 304 ++++++++++++++++++ src/services/soundtouchplus-service.ts | 37 ++- src/types.ts | 23 -- src/types/cardconfig.ts | 177 ++++++++-- .../hassentityattributes-soundtouchplus.ts | 3 + src/types/mediaplayer-entityfeature.ts | 22 ++ src/types/section.ts | 5 +- src/utils/media-browser-utils.ts | 127 ++++++-- src/utils/utils.ts | 2 - 40 files changed, 3153 insertions(+), 750 deletions(-) create mode 100644 data/recently_played_cache_9070658C9D4A.xml create mode 100644 src/components/player-controls.ts create mode 100644 src/components/player-header.ts create mode 100644 src/components/player-progress.ts create mode 100644 src/components/player-volume.ts create mode 100644 src/editor/player-controls-editor.ts create mode 100644 src/editor/player-editor.ts create mode 100644 src/editor/player-header-editor.ts create mode 100644 src/editor/player-volume-editor.ts create mode 100644 src/editor/source-browser-editor.ts create mode 100644 src/sections/source-browser.ts create mode 100644 src/services/media-control-service.ts create mode 100644 src/types/mediaplayer-entityfeature.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7504553..4200db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Change are listed in reverse chronological order (newest to oldest). +###### [ 1.0.3 ] - 2024/05/20 + + * Updated the Player section to be a basic media player; more features are planned, but I wanted to get something out there that was functional. + * Added Sources section to allow media player source to be selected. + * Added recently played list cache support, that caches recently played content to the local file system. The cache list is preferred over the SoundTouch device list, since the device removes it's cover art image links quite frequently. Max cache items is configurable in the SoundTouchPlus device options (per device). + * Updated wiki documentation. + +###### [ 1.0.2 ] - 2024/05/12 + + * Updated `README.MD` HACS documentation file. + ###### [ 1.0.1 ] - 2024/05/12 * Removed `console.log` messages that were being used for testing. diff --git a/SoundTouchPlusCard.njsproj b/SoundTouchPlusCard.njsproj index 03405c7..018571d 100644 --- a/SoundTouchPlusCard.njsproj +++ b/SoundTouchPlusCard.njsproj @@ -5,7 +5,7 @@ 2.0 {19dfee94-5cae-417c-ac13-38b165e4121e} . - ShowAllFiles + ProjectFiles dist\soundtouchplus-card.js . . @@ -34,6 +34,9 @@ + + Code + Code @@ -76,6 +79,7 @@ + @@ -89,10 +93,23 @@ + + + Code + + + + + + + + + + Code @@ -102,6 +119,9 @@ + + Code + Code diff --git a/data/recently_played_cache_9070658C9D4A.xml b/data/recently_played_cache_9070658C9D4A.xml new file mode 100644 index 0000000..5c10373 --- /dev/null +++ b/data/recently_played_cache_9070658C9D4A.xml @@ -0,0 +1,33 @@ + + + + + Write Your Story + https://i.scdn.co/image/ab67616d0000b273e3766f8c2f988da274a3b104 + + + + + King of My Heart + https://i.scdn.co/image/ab67616d0000b27345e47016d5d5aa45198e7a89 + + + + + Speak Life + https://i.scdn.co/image/ab67616d0000b27365829d58a159958643aa33c0 + + + + + Royal Blood + https://i.scdn.co/image/ab67616d0000b2738ce608af9fc8281be7f494d2 + + + + + One Of These Days + https://i.scdn.co/image/ab67616d0000b27327f15cd881e33e8c23b0bc5e + + + \ No newline at end of file diff --git a/src/card.ts b/src/card.ts index a824420..b9ba2e7 100644 --- a/src/card.ts +++ b/src/card.ts @@ -17,9 +17,15 @@ import { PROGRESS_DONE, PROGRESS_STARTED, SECTION_SELECTED } from './constants'; import { formatTitleInfo, removeSpecialChars } from './utils/media-browser-utils'; import { isNumber } from './utils/utils'; -//const LOGPFX = "STPC - card." -const { PRESETS, RECENTS, PANDORA_STATIONS, PLAYER } = Section; +const { + PANDORA_STATIONS, + PLAYER, + PRESETS, + RECENTS, + SOURCES +} = Section; + const HEADER_HEIGHT = 2; const FOOTER_HEIGHT = 4; const CARD_DEFAULT_HEIGHT = '35.15rem'; @@ -27,8 +33,8 @@ const CARD_DEFAULT_WIDTH = '35.15rem'; const CARD_EDIT_PREVIEW_HEIGHT = '42rem'; const CARD_EDIT_PREVIEW_WIDTH = '100%'; -const PARENTELEMENT_TAGNAME_HUI_CARD_OPTIONS = 'HUI-CARD-OPTIONS'; -const PARENTELEMENT_TAGNAME_HUI_CARD_PREVIEW = 'HUI-CARD-PREVIEW'; +//const PARENTELEMENT_TAGNAME_HUI_CARD_OPTIONS = 'HUI-CARD-OPTIONS'; +//const PARENTELEMENT_TAGNAME_HUI_CARD_PREVIEW = 'HUI-CARD-PREVIEW'; const EDIT_TAB_HEIGHT = '48px'; const EDIT_BOTTOM_TOOLBAR_HEIGHT = '59px'; @@ -84,7 +90,7 @@ export class Card extends LitElement { */ protected render(): TemplateResult | void { - //console.log(LOGPFX + "render()\n Rendering card"); + //console.log("card.render()\n Rendering card"); // just in case hass property has not been set yet. if (!this.hass) @@ -102,7 +108,7 @@ export class Card extends LitElement { const showFooter = !sections || sections.length > 1; const title = formatTitleInfo(this.config.title, this.config, this.store.player); - //console.log(LOGPFX + "render():\n this.section=%s", JSON.stringify(this.section)) + //console.log("card.render():\n this.section=%s", JSON.stringify(this.section)) // render html for the card. return html` @@ -111,14 +117,15 @@ export class Card extends LitElement { ${title ? html`
${title}
` : html``} -
+
${ this.playerId ? choose(this.section, [ - [PRESETS, () => html` `], - [RECENTS, () => html` `], [PANDORA_STATIONS, () => html` `], [PLAYER, () => html` `], + [PRESETS, () => html` `], + [RECENTS, () => html` `], + [SOURCES, () => html` `], ]) : html`
Player not configured
` } @@ -137,6 +144,116 @@ export class Card extends LitElement { } + /** + * Style definitions used by this card. + */ + static get styles() { + return css` + :host { + display: inline-block; + width: 100% !important; + height: 100% !important; + } + + * { + margin: 0; + } + + html, + body { + height: 100%; + margin: 0; + } + + soundtouchplus-card { + display: block; + height: 100% !important; + width: 100% !important; + } + + hui-card-preview { + min-height: 10rem; + height: 40rem; + min-width: 10rem; + width: 40rem; + } + + .stpc-card { + --stpc-card-header-height: ${HEADER_HEIGHT}rem; + --stpc-card-footer-height: ${FOOTER_HEIGHT}rem; + --stpc-card-edit-tab-height: 0px; + --stpc-card-edit-bottom-toolbar-height: 0px; + box-sizing: border-box; + color: var(--secondary-text-color); + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 20rem; + height: calc(100vh - var(--stpc-card-footer-height) - var(--stpc-card-edit-tab-height) - var(--stpc-card-edit-bottom-toolbar-height)); + min-width: 20rem; + width: calc(100vw - var(--mdc-drawer-width)); + } + + .stpc-card-header { + margin: 0.2rem; + display: flex; + align-self: flex-start; + align-items: center; + justify-content: space-around; + width: 100%; + font-weight: bold; + font-size: 1.2rem; + color: var(--secondary-text-color); + } + + .stpc-card-content-section { + margin: 0.0rem; + flex-grow: 1; + flex-shrink: 0; + height: 1vh; + overflow: hidden; + } + + .stpc-card-footer { + margin: 0.2rem; + display: flex; + align-self: flex-start; + align-items: center; + justify-content: space-around; + width: 100%; + --mdc-icon-size: 1.75rem; + --mdc-icon-button-size: 2.5rem; + --mdc-ripple-top: 0px; + --mdc-ripple-left: 0px; + --mdc-ripple-fg-size: 10px; + } + + .stpc-loader { + position: absolute; + z-index: 1000; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + --mdc-theme-primary: var(--dark-primary-color); + } + + .stpc-not-configured { + text-align: center; + margin-top: 50%; + } + + ha-icon-button { + padding-left: 1rem; + padding-right: 1rem; + } + + ha-circular-progress { + --md-sys-color-primary: var(--dark-primary-color); + } + `; + } + + /** * Creates the common services and data areas that are used by the various card sections. * @@ -211,7 +328,7 @@ export class Card extends LitElement { this.cancelLoader = true; const duration = Date.now() - this.loaderTimestamp; - //console.log(LOGPFX + "OnProgressDone()\nHiding progress indicator - duration=%s, this.showLoader=%s", JSON.stringify(duration), JSON.stringify(this.showLoader)); + //console.log("card.OnProgressDone()\nHiding progress indicator - duration=%s, this.showLoader=%s", JSON.stringify(duration), JSON.stringify(this.showLoader)); if (this.showLoader) { if (duration < 1000) { setTimeout(() => (this.showLoader = false), 1000 - duration); @@ -264,6 +381,7 @@ export class Card extends LitElement { protected OnSectionSelected = (args: Event) => { const sectionToSelect = (args as CustomEvent).detail as Section; + //console.log("card.OnSectionSelected() - sectionToSelect:\n%s", sectionToSelect); // is section activated? if so, then select it. if (this.config.sections?.includes(sectionToSelect)) { @@ -286,10 +404,13 @@ export class Card extends LitElement { //console.log("OnShowSection\n this.config.sections=%s\n event section=%s\n this.section=%s", JSON.stringify(this.config.sections), JSON.stringify(args.detail), JSON.stringify(this.section)); const section = args.detail; + //console.log("card.OnShowSection()\n this.section=%s", JSON.stringify(section)); if (!this.config.sections || this.config.sections.indexOf(section) > -1) { this.section = section; - //console.log("card.OnShowSection()\n changed section - this.section=%s", JSON.stringify(this.section)); + //this.requestUpdate(); + } else { + console.log("STPC - card.OnShowSection()\n section is not active: %s", JSON.stringify(section)); } } @@ -348,12 +469,19 @@ export class Card extends LitElement { // remove any configuration properties that do not have a value set. for (const [key, value] of Object.entries(newConfig)) { if (Array.isArray(value) && value.length === 0) { - //console.log(LOGPFX + "setConfig()\n Removing empty value config key '%s'", key) + //console.log("card.setConfig()\n Removing empty value config key '%s'", key) delete newConfig[key]; } } // default configration values if not set. + newConfig.playerHeaderHide = newConfig.playerHeaderHide || false; + newConfig.playerHeaderHideProgressBar = newConfig.playerHeaderHideProgressBar || false; + newConfig.playerControlsHidePlayPause = newConfig.playerControlsHidePlayPause || false; + newConfig.playerControlsHideRepeat = newConfig.playerControlsHideRepeat || false; + newConfig.playerControlsHideShuffle = newConfig.playerControlsHideShuffle || false; + newConfig.playerControlsHideTrackNext = newConfig.playerControlsHideTrackNext || false; + newConfig.playerControlsHideTrackPrev = newConfig.playerControlsHideTrackPrev || false; newConfig.presetBrowserItemsPerRow = newConfig.presetBrowserItemsPerRow || 3; newConfig.presetBrowserItemsHideSource = newConfig.presetBrowserItemsHideSource || false; newConfig.presetBrowserItemsHideTitle = newConfig.presetBrowserItemsHideTitle || false; @@ -362,6 +490,7 @@ export class Card extends LitElement { newConfig.recentBrowserItemsHideTitle = newConfig.recentBrowserItemsHideTitle || false; newConfig.pandoraBrowserItemsPerRow = newConfig.pandoraBrowserItemsPerRow || 9; newConfig.pandoraBrowserItemsHideTitle = newConfig.pandoraBrowserItemsHideTitle || false; + newConfig.sourceBrowserItemsPerRow = newConfig.sourceBrowserItemsPerRow || 3; // if custom imageUrl's are supplied, then remove special characters from each title // to speed up comparison when imageUrl's are loaded later on. we will also @@ -450,24 +579,30 @@ export class Card extends LitElement { sections: [Section.PRESETS, Section.RECENTS], entity: "", title: 'SoundTouch Card "{player.name}"', + playerHeaderTitle: '{player.name}', + playerHeaderArtistTrack: '{player.media_artist} - {player.media_title}', + playerHeaderAlbum: '{player.media_album_name}', + playerHeaderNoMediaPlayingText: 'no media is playing', + sourceBrowserTitle: '"{player.name}" Sources ({medialist.itemcount} items)', + sourceBrowserSubTitle: 'click an item to select the source', + sourceBrowserItemsPerRow: 1, presetBrowserTitle: '"{player.name}" Presets', - presetBrowserSubTitle: "last updated on {player.soundtouchplus_presets_lastupdated}", + presetBrowserSubTitle: "last updated on {player.soundtouchplus_presets_lastupdated} ({medialist.itemcount} items)", presetBrowserItemsPerRow: 3, presetBrowserItemsHideTitle: false, presetBrowserItemsHideSource: false, recentBrowserTitle: '"{player.name}" Recently Played', - recentBrowserSubTitle: "last updated on {player.soundtouchplus_recents_lastupdated}", - recentBrowserItemsPerRow: 10, + recentBrowserSubTitle: "last updated on {player.soundtouchplus_recents_cache_lastupdated} ({medialist.itemcount} items)", + recentBrowserItemsPerRow: 4, recentBrowserItemsHideTitle: false, recentBrowserItemsHideSource: false, pandoraBrowserTitle: '"{player.name}" My Pandora Stations', - pandoraBrowserSubTitle: "refreshed on {lastupdatedon}", - pandoraBrowserItemsPerRow: 9, + pandoraBrowserSubTitle: "refreshed on {lastupdatedon} ({medialist.itemcount} items)", + pandoraBrowserItemsPerRow: 4, pandoraBrowserItemsHideTitle: false, customImageUrls: { "default": "/local/images/soundtouchplus_card_customimages/default.png", "empty preset": "/local/images/soundtouchplus_card_customimages/empty_preset.png", - "My Private Playlist": "/local/images/soundtouchplus_card_customimages/logo_spotify.png", "Daily Mix 1": "https://brands.home-assistant.io/spotifyplus/icon.png", } } @@ -490,7 +625,7 @@ export class Card extends LitElement { // are we previewing the card in the card editor? // if so, then we will ignore the configuration dimensions and use constants. - if (this.parentElement?.tagName == PARENTELEMENT_TAGNAME_HUI_CARD_PREVIEW) { + if (this.store.isInCardEditPreview()) { //console.log("card.styleCard() - card is in edit preview"); cardHeight = CARD_EDIT_PREVIEW_HEIGHT; cardWidth = CARD_EDIT_PREVIEW_WIDTH; @@ -505,7 +640,7 @@ export class Card extends LitElement { // set card editor options. // we have to account for various editor toolbars in the height calculations when using 'fill' mode. // we do not have to worry about width calculations, as the width is the same with or without edit mode. - if (this.parentElement?.tagName == PARENTELEMENT_TAGNAME_HUI_CARD_OPTIONS) { + if (this.store.isInDashboardEditor()) { //console.log("card.styleCard() width - dashboard is in edit mode"); editTabHeight = EDIT_TAB_HEIGHT; editBottomToolbarHeight = EDIT_BOTTOM_TOOLBAR_HEIGHT; @@ -550,102 +685,4 @@ export class Card extends LitElement { width: `${cardWidth ? cardWidth : undefined}`, }); } - - - /** - * Style definitions used by this card. - */ - static get styles() { - return css` - :host { - display: inline-block; - width: 100% !important; - height: 100% !important; - } - - * { - margin: 0; - } - - html, - body { - height: 100%; - margin: 0; - } - - soundtouchplus-card { - display: block; - height: 100% !important; - width: 100% !important; - } - - hui-card-preview { - min-height: 10rem; - height: 40rem; - min-width: 10rem; - width: 40rem; - } - - .stpc-card { - --stpc-card-header-height: ${HEADER_HEIGHT}rem; - --stpc-card-footer-height: ${FOOTER_HEIGHT}rem; - --stpc-card-edit-tab-height: 0px; - --stpc-card-edit-bottom-toolbar-height: 0px; - box-sizing: border-box; - color: var(--secondary-text-color); - overflow: hidden; - display: flex; - flex-direction: column; - min-height: 20rem; - height: calc(100vh - var(--stpc-card-footer-height) - var(--stpc-card-edit-tab-height) - var(--stpc-card-edit-bottom-toolbar-height)); - min-width: 20rem; - width: calc(100vw - var(--mdc-drawer-width)); - } - - .stpc-card-header { - margin: 0.2rem 0; - text-align: center; - align-content: center; - font-weight: bold; - font-size: 1.2rem; - color: var(--secondary-text-color); - height: var(--stpc-card-header-height); - } - - .stpc-card-content { - overflow-y: auto; - flex: 1; - } - - .stpc-card-footer { - padding-top: 0.3rem; - padding-left: 0.3rem; - padding-right: 0.3rem; - height: var(--stpc-card-footer-height); - } - - .stpc-loader { - position: absolute; - z-index: 1000; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - --mdc-theme-primary: var(--accent-color); - } - - .stpc-not-configured { - text-align: center; - margin-top: 50%; - } - - ha-icon-button { - padding-left: 1rem; - padding-right: 1rem; - } - - ha-circular-progress { - --md-sys-color-primary: var(--accent-color); - } - `; - } } diff --git a/src/components/footer.ts b/src/components/footer.ts index 9a2d216..4d9fcd8 100644 --- a/src/components/footer.ts +++ b/src/components/footer.ts @@ -1,7 +1,13 @@ // lovelace card imports. import { css, html, LitElement, TemplateResult, nothing } from 'lit'; import { property } from 'lit/decorators.js'; -import { mdiHome, mdiStarOutline, mdiHistory, mdiPandora } from '@mdi/js'; +import { + mdiAudioInputRca, + mdiHistory, + mdiPandora, + mdiPlayCircle, + mdiStarOutline, +} from '@mdi/js'; // our imports. import { SHOW_SECTION } from '../constants'; @@ -10,7 +16,13 @@ import { Section } from '../types/section' import { customEvent } from '../utils/utils'; -const { PRESETS, RECENTS, PANDORA_STATIONS, PLAYER } = Section; +const { + PANDORA_STATIONS, + PLAYER, + PRESETS, + RECENTS, + SOURCES, +} = Section; class Footer extends LitElement { @@ -26,54 +38,53 @@ class Footer extends LitElement { return html` this.dispatchSection(PLAYER)} - selected=${this.selected(PLAYER)} + .path=${mdiPlayCircle} + .label="Player" + @click=${() => this.OnSectionClick(PLAYER)} + selected=${this.setSection(PLAYER)} + hide=${this.getSectionEnabled(PLAYER)} + > + this.OnSectionClick(SOURCES)} + selected=${this.setSection(SOURCES)} + hide=${this.getSectionEnabled(SOURCES)} > this.dispatchSection(PRESETS)} - selected=${this.selected(PRESETS)} + .label="Presets" + @click=${() => this.OnSectionClick(PRESETS)} + selected=${this.setSection(PRESETS)} + hide=${this.getSectionEnabled(PRESETS)} > this.dispatchSection(RECENTS)} - selected=${this.selected(RECENTS)} + .label="Recently Played" + @click=${() => this.OnSectionClick(RECENTS)} + selected=${this.setSection(RECENTS)} + hide=${this.getSectionEnabled(RECENTS)} > this.dispatchSection(PANDORA_STATIONS)} - selected=${this.selected(PANDORA_STATIONS)} + .label='Pandora Stations' + .hideTitle=false + .ariaHasPopup=true + @click=${() => this.OnSectionClick(PANDORA_STATIONS)} + selected=${this.setSection(PANDORA_STATIONS)} + hide=${this.getSectionEnabled(PANDORA_STATIONS)} > `; } - private dispatchSection(section: Section) { - this.dispatchEvent(customEvent(SHOW_SECTION, section)); - } - - private selected(section: Section | typeof nothing) { - return this.section === section || nothing; - } - - - private hide(searchElement: Section) { - return (this.config.sections && !this.config.sections?.includes(searchElement)) || nothing; - } - - /** * Style definitions used by this card section. */ static get styles() { return css` :host > *[selected] { - color: var(--accent-color); + color: var(--dark-primary-color); } :host > *[hide] { @@ -87,30 +98,36 @@ class Footer extends LitElement { `; } -// /** -// * Style definitions used by this card section. -// */ -// static get styles() { -// return css` -// :host { -// display: flex; -// justify-content: space-between; -// } -// :host > * { -// padding: 0; -// } -// :host > *[selected] { -// color: var(--accent-color); -// } -// :host > *[hide] { -// display: none; -// } -// .ha-icon-button { -// --mwc-icon-button-size: 3rem; -// --mwc-icon-size: 2rem; -// } -// `; -// } + + /** + * Handles the `click` event fired when a section icon is clicked. + * + * @param section Event arguments. + */ + private OnSectionClick(section: Section) { + this.dispatchEvent(customEvent(SHOW_SECTION, section)); + } + + + /** + * Stores a reference to the selected section. + * + * @param section Section identifier to store. + */ + private setSection(section: Section | typeof nothing) { + return this.section === section || nothing; + } + + + /** + * Returns nothing if the specified section value is NOT enabled in the configuration, + * which will cause the section icon to be hidden (via css styling). + * + * @param section Section identifier to check. + */ + private getSectionEnabled(searchElement: Section) { + return (this.config.sections && !this.config.sections?.includes(searchElement)) || nothing; + } } customElements.define('stpc-footer', Footer); diff --git a/src/components/ha-player.ts b/src/components/ha-player.ts index cf934f4..ebc3024 100644 --- a/src/components/ha-player.ts +++ b/src/components/ha-player.ts @@ -4,7 +4,7 @@ import { property } from 'lit/decorators.js'; // our imports. import { Store } from '../model/store'; -import { MediaPlayerEntityFeature } from '../types'; +import { MediaPlayerEntityFeature } from '../types/mediaplayer-entityfeature'; class HaPlayer extends LitElement { @@ -18,7 +18,10 @@ class HaPlayer extends LitElement { */ protected render(): TemplateResult | void { + // get current media player state. const state = this.store.hass.states[this.store.player.id]; + + // load all features supported by the player. let supportedFeatures = 0; this.features.forEach((feature) => (supportedFeatures += feature)); @@ -26,7 +29,13 @@ class HaPlayer extends LitElement { ...state, attributes: { ...state.attributes, supported_features: supportedFeatures }, }; - return html` `; + + // render content. + return html` + + `; } } diff --git a/src/components/media-browser-icons.ts b/src/components/media-browser-icons.ts index d2765a1..3d7deab 100644 --- a/src/components/media-browser-icons.ts +++ b/src/components/media-browser-icons.ts @@ -63,6 +63,10 @@ export class MediaBrowserIcons extends LitElement { itemsPerRow = this.config.pandoraBrowserItemsPerRow || 3; hideTitle = this.config.pandoraBrowserItemsHideTitle || false; hideSource = true; + } else if (this.section == Section.SOURCES) { + itemsPerRow = this.config.sourceBrowserItemsPerRow || 3; + hideTitle = this.config.sourceBrowserItemsHideTitle || false; + hideSource = true; } return html` @@ -74,7 +78,7 @@ export class MediaBrowserIcons extends LitElement {
${itemsWithFallbacks(this.items, this.config).map( (item, index) => html` - ${styleMediaBrowserItemBackgroundImage(item.thumbnail, index)} + ${styleMediaBrowserItemBackgroundImage(item.thumbnail, index, this.section)} this.buttonMediaBrowserItemClick(customEvent(ITEM_SELECTED, item))} @@ -89,42 +93,6 @@ export class MediaBrowserIcons extends LitElement { } - /** - * Event fired when a mousedown event takes place for a media browser item button. - * In this case, we will store the current time (in milliseconds) so that we can calculate - * the duration in the "click" event (occurs after a mouseup event). - * - * @param event Event arguments. - */ - private buttonMediaBrowserItemMouseDown(): boolean { - // store when the mouse down event took place. - this.mousedownTimestamp = Date.now(); - return true; - } - - - /** - * Event fired when a click event takes place for a media browser item button. - * - * In this case, we are looking to determine how long the mouse button was in the - * down position (e.g. the duration). If the duration was greater than 1500 milliseconds, - * then we will treat the event as a "click and hold" operation; otherwise, we will treat - * the event as a "click" operation. - * - * @param event Event arguments. - */ - private buttonMediaBrowserItemClick(event: CustomEvent): boolean { - // calculate the duration of the mouse down / up operation. - const duration = Date.now() - this.mousedownTimestamp; - this.mousedownTimestamp = 0; - if (duration < 1500) { - return this.dispatchEvent(event); - } else { - return this.dispatchEvent(customEvent(ITEM_SELECTED_WITH_HOLD, event.detail)); - } - } - - /** * Style definitions used by this card section. * @@ -168,15 +136,48 @@ export class MediaBrowserIcons extends LitElement { .title-source { font-size: 0.8rem; - //position: absolute; width: 100%; line-height: 160%; - //bottom: 0; - //background-color: rgba(var(--rgb-card-background-color), 0.733); } `, ]; } + + + /** + * Event fired when a mousedown event takes place for a media browser item button. + * In this case, we will store the current time (in milliseconds) so that we can calculate + * the duration in the "click" event (occurs after a mouseup event). + * + * @param event Event arguments. + */ + private buttonMediaBrowserItemMouseDown(): boolean { + // store when the mouse down event took place. + this.mousedownTimestamp = Date.now(); + return true; + } + + + /** + * Event fired when a click event takes place for a media browser item button. + * + * In this case, we are looking to determine how long the mouse button was in the + * down position (e.g. the duration). If the duration was greater than 1500 milliseconds, + * then we will treat the event as a "click and hold" operation; otherwise, we will treat + * the event as a "click" operation. + * + * @param event Event arguments. + */ + private buttonMediaBrowserItemClick(event: CustomEvent): boolean { + // calculate the duration of the mouse down / up operation. + const duration = Date.now() - this.mousedownTimestamp; + this.mousedownTimestamp = 0; + if (duration < 1500) { + return this.dispatchEvent(event); + } else { + return this.dispatchEvent(customEvent(ITEM_SELECTED_WITH_HOLD, event.detail)); + } + } } customElements.define('stpc-media-browser-icons', MediaBrowserIcons); diff --git a/src/components/media-browser-list.ts b/src/components/media-browser-list.ts index 328bb4e..32fca8d 100644 --- a/src/components/media-browser-list.ts +++ b/src/components/media-browser-list.ts @@ -52,6 +52,7 @@ export class MediaBrowserList extends LitElement { let hideTitle = true; let hideSource = true; let itemsPerRow = 1; + let listItemClass = 'button'; if (this.section == Section.PRESETS) { itemsPerRow = this.config.presetBrowserItemsPerRow || 3; hideTitle = this.config.presetBrowserItemsHideTitle || false; @@ -64,8 +65,15 @@ export class MediaBrowserList extends LitElement { itemsPerRow = this.config.pandoraBrowserItemsPerRow || 3; hideTitle = this.config.pandoraBrowserItemsHideTitle || false; hideSource = true; + } else if (this.section == Section.SOURCES) { + itemsPerRow = this.config.sourceBrowserItemsPerRow || 3; + hideTitle = this.config.sourceBrowserItemsHideTitle || false; + hideSource = true; + // make the source icons half the size of regular list buttons. + listItemClass += ' button-source'; } + // render html. return html` `; @@ -276,6 +337,4 @@ export function renderMediaBrowserContentItem(contentItem: ContentItem | undefin
${formatStringProperCase(contentItem?.Source || '')}
`; - //
- //
} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ba56a24..0be32d1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -78,5 +78,3 @@ export function formatStringProperCase(str: string): string | void { export function isNumber(numStr: string) { return !isNaN(parseFloat(numStr)) && !isNaN(+numStr) } - -