From b857539b557787a8a3f0ad87c82810690da5cef1 Mon Sep 17 00:00:00 2001 From: Todd Lucas Date: Fri, 7 Feb 2025 10:31:47 -0600 Subject: [PATCH] [ 1.0.38 ] * Added card configuration option `searchMediaBrowserQueueSelection`; True to add track / episode to play queue when search result is clicked; Otherwise, false to play the track / episode immediately when track search result is clicked. Default is false. * Added theme variable `--spc-card-wait-progress-slider-color` and `cardWaitProgressSliderColor` card configuration option; Color of the card area wait progress indicator (default `#2196F3`). * Added theme variable `--spc-player-progress-label-color` and `playerProgressLabelColor` card configuration option; Color of the player progress text labels (default `#ffffff`). * Added theme variable `--spc-player-progress-slider-color` and `playerProgressSliderColor` card configuration option; Color of the player progress slider bar (default `#2196F3`). * Added theme variable `--spc-player-volume-label-color` and `playerVolumeLabelColor` card configuration option; Color of the player volume text labels (default `#ffffff`). * Added theme variable `--spc-player-volume-slider-color` and `playerVolumeSliderColor` card configuration option; Color of the player volume slider bar (default `#2196F3`). * Corrected a bug when displaying player queue; the queue would not render when it contained a mix of tracks, podcast episodes, and audiobook chapters. Queue will now be displayed on the type of items in the queue, and not based on what the player is currently playing. --- CHANGELOG.md | 10 +++ src/card.ts | 55 ++++++------ src/components/player-body-queue.ts | 101 ++++++++-------------- src/components/player-progress.ts | 4 +- src/components/player-volume.ts | 6 +- src/constants.ts | 4 +- src/editor/search-media-browser-editor.ts | 13 +-- src/sections/fav-browser-base.ts | 36 ++++++++ src/sections/player.ts | 13 ++- src/sections/search-media-browser.ts | 29 +++++++ src/types/card-config.ts | 37 ++++++++ 11 files changed, 198 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3adc54..c25e5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ Change are listed in reverse chronological order (newest to oldest). +###### [ 1.0.38 ] - 2025/02/07 + + * Added card configuration option `searchMediaBrowserQueueSelection`; True to add track / episode to play queue when search result is clicked; Otherwise, false to play the track / episode immediately when track search result is clicked. Default is false. + * Added theme variable `--spc-card-wait-progress-slider-color` and `cardWaitProgressSliderColor` card configuration option; Color of the card area wait progress indicator (default `#2196F3`). + * Added theme variable `--spc-player-progress-label-color` and `playerProgressLabelColor` card configuration option; Color of the player progress text labels (default `#ffffff`). + * Added theme variable `--spc-player-progress-slider-color` and `playerProgressSliderColor` card configuration option; Color of the player progress slider bar (default `#2196F3`). + * Added theme variable `--spc-player-volume-label-color` and `playerVolumeLabelColor` card configuration option; Color of the player volume text labels (default `#ffffff`). + * Added theme variable `--spc-player-volume-slider-color` and `playerVolumeSliderColor` card configuration option; Color of the player volume slider bar (default `#2196F3`). + * Corrected a bug when displaying player queue; the queue would not render when it contained a mix of tracks, podcast episodes, and audiobook chapters. Queue will now be displayed on the type of items in the queue, and not based on what the player is currently playing. + ###### [ 1.0.37 ] - 2025/02/05 * Updated favorite browsers to clear cached filter criteria if the filter criteria textbox was cleared. diff --git a/src/card.ts b/src/card.ts index 89dfccf..3dd2a6e 100644 --- a/src/card.ts +++ b/src/card.ts @@ -331,7 +331,7 @@ export class Card extends LitElement { top: 50%; left: 50%; transform: translate(-50%, -50%); - --mdc-theme-primary: var(--dark-primary-color); + --mdc-theme-primary: var(--spc-card-wait-progress-slider-color, var(--dark-primary-color, #2196F3)); } .spc-not-configured { @@ -350,7 +350,7 @@ export class Card extends LitElement { } ha-circular-progress { - --md-sys-color-primary: var(--dark-primary-color); + --md-sys-color-primary: var(--spc-card-wait-progress-slider-color, var(--dark-primary-color, #2196F3)); } `; } @@ -1152,44 +1152,42 @@ export class Card extends LitElement { let cardHeight: string | undefined = undefined; let editTabHeight = '0px'; let editBottomToolbarHeight = '0px'; + const cardWaitProgressSliderColor = this.config.cardWaitProgressSliderColor; + + // build style info object. + const styleInfo: StyleInfo = {}; + if (cardWaitProgressSliderColor) + styleInfo['--spc-card-wait-progress-slider-color'] = `${cardWaitProgressSliderColor}`; // are we previewing the card in the card editor? // if so, then we will ignore the configuration dimensions and use constants. if (isCardInEditPreview(this)) { // card is in edit preview. - cardHeight = CARD_EDIT_PREVIEW_HEIGHT; - cardWidth = CARD_EDIT_PREVIEW_WIDTH; - return styleMap({ - '--spc-card-edit-tab-height': `${editTabHeight}`, - '--spc-card-edit-bottom-toolbar-height': `${editBottomToolbarHeight}`, - height: `${cardHeight ? cardHeight : undefined}`, - width: `${cardWidth ? cardWidth : undefined}`, - 'background-repeat': `${!this.playerId ? 'no-repeat' : undefined}`, - 'background-position': `${!this.playerId ? 'center' : undefined}`, - 'background-image': `${!this.playerId ? 'url(' + BRAND_LOGO_IMAGE_BASE64 + ')' : undefined}`, - 'background-size': `${!this.playerId ? BRAND_LOGO_IMAGE_SIZE : undefined}`, - }); - + styleInfo['--spc-card-edit-tab-height'] = `${editTabHeight}`; + styleInfo['--spc-card-edit-bottom-toolbar-height'] = `${editBottomToolbarHeight}`; + styleInfo['height'] = `${CARD_EDIT_PREVIEW_HEIGHT}`; + styleInfo['width'] = `${CARD_EDIT_PREVIEW_WIDTH}`; + styleInfo['background-repeat'] = `${!this.playerId ? 'no-repeat' : undefined}`; + styleInfo['background-position'] = `${!this.playerId ? 'center' : undefined}`; + styleInfo['background-image'] = `${!this.playerId ? 'url(' + BRAND_LOGO_IMAGE_BASE64 + ')' : undefined}`; + styleInfo['background-size'] = `${!this.playerId ? BRAND_LOGO_IMAGE_SIZE : undefined}`; + return styleMap(styleInfo); } // set card picker options. if (isCardInPickerPreview(this)) { // card is in pick preview. - cardHeight = CARD_PICK_PREVIEW_HEIGHT; - cardWidth = CARD_PICK_PREVIEW_WIDTH; - return styleMap({ - '--spc-card-edit-tab-height': `${editTabHeight}`, - '--spc-card-edit-bottom-toolbar-height': `${editBottomToolbarHeight}`, - height: `${cardHeight ? cardHeight : undefined}`, - width: `${cardWidth ? cardWidth : undefined}`, - 'background-repeat': `no-repeat`, - 'background-position': `center`, - 'background-image': `url(${BRAND_LOGO_IMAGE_BASE64})`, - 'background-size': `${BRAND_LOGO_IMAGE_SIZE}`, - }); - + styleInfo['--spc-card-edit-tab-height'] = `${editTabHeight}`; + styleInfo['--spc-card-edit-bottom-toolbar-height'] = `${editBottomToolbarHeight}`; + styleInfo['height'] = `${CARD_PICK_PREVIEW_HEIGHT}`; + styleInfo['width'] = `${CARD_PICK_PREVIEW_WIDTH}`; + styleInfo['background-repeat'] = 'no-repeat'; + styleInfo['background-position'] = 'center'; + styleInfo['background-image'] = `url(${BRAND_LOGO_IMAGE_BASE64})`; + styleInfo['background-size'] = `${BRAND_LOGO_IMAGE_SIZE}`; + return styleMap(styleInfo); } // set card editor options. @@ -1235,7 +1233,6 @@ export class Card extends LitElement { //); // build style info object. - const styleInfo: StyleInfo = {}; styleInfo['--spc-card-edit-tab-height'] = `${editTabHeight}`; styleInfo['--spc-card-edit-bottom-toolbar-height'] = `${editBottomToolbarHeight}`; styleInfo['height'] = `${cardHeight ? cardHeight : undefined}`; diff --git a/src/components/player-body-queue.ts b/src/components/player-body-queue.ts index 979ef0d..bd3ec97 100644 --- a/src/components/player-body-queue.ts +++ b/src/components/player-body-queue.ts @@ -25,7 +25,6 @@ import { ITrack } from '../types/spotifyplus/track'; * Track actions. */ enum Actions { - ChapterPlay = "ChapterPlay", EpisodePlay = "EpisodePlay", GetPlayerQueueInfo = "GetPlayerQueueInfo", TrackPlay = "TrackPlay", @@ -49,67 +48,40 @@ export class PlayerBodyQueue extends PlayerBodyBase { super.render(); // initialize common elements. - let queueInfoTitle = html`Unknown`; - let queueInfoParentTitle = html`Title Type`; let queueItems = html`
No items found in Queue
`; - // generate queue items info based on content type. - if (this.player.attributes.sp_item_type == 'podcast') { - - // build queue info display for podcast episodes. - queueInfoTitle = html`Episodes`; - queueInfoParentTitle = html`Show`; - if ((this.queueInfo?.queue || []).length > 0) { - queueItems = html`${this.queueInfo?.queue.map((item, index) => html` - this.onClickAction(Actions.TrackPlay, item)} - slot="icon-button" - >  -
${index + 1}
-
${item.name || ""}
-
${item.artists[0].name || ""}
- `)}`; - } - - } else if (this.player.attributes.sp_item_type == 'audiobook') { - - // build queue info display for audiobook chapters. - queueInfoTitle = html`Chapters`; - queueInfoParentTitle = html`Audiobook`; - if ((this.queueInfo?.queue || []).length > 0) { - queueItems = html`${this.queueInfo?.queue.map((item, index) => html` - this.onClickAction(Actions.ChapterPlay, item)} - slot="icon-button" - >  -
${index + 1}
-
${item.name || ""}
-
${item.show?.name || ""}
- `)}`; - } - - } else if (this.player.attributes.sp_item_type == 'track') { - - // build queue info display for tracks. - queueInfoTitle = html`Tracks`; - queueInfoParentTitle = html`Artist`; - if ((this.queueInfo?.queue || []).length > 0) { - queueItems = html`${this.queueInfo?.queue.map((item, index) => html` - this.onClickAction(Actions.TrackPlay, item)} - slot="icon-button" - >  -
${index + 1}
-
${item.name || ""}
-
${item.artists[0].name || ""}
- `)}`; - } + // process all queue items. + if ((this.queueInfo?.queue || []).length > 0) { + queueItems = html`${this.queueInfo?.queue.map((item, index) => html` + ${(() => { + // render based on item type (track, episode). + if (item.type == 'episode') { + return (html ` + this.onClickAction(Actions.EpisodePlay, item)} + slot="icon-button" + >  +
${index + 1}
+
${item.name || ""}
+
${item.show?.name || ""}
+ `) + } else { + return (html ` + this.onClickAction(Actions.TrackPlay, item)} + slot="icon-button" + >  +
${index + 1}
+
${item.name || ""}
+
${item.artists[0].name || ""}
+ `) + } + })()} + `)}`; } // render html. @@ -119,14 +91,14 @@ export class PlayerBodyQueue extends PlayerBodyBase { ${this.alertError ? html`${this.alertError}` : ""} ${this.alertInfo ? html`${this.alertInfo}` : ""}
- Player Queue Info - ${queueInfoTitle} + Player Queue Items
 
#
Title
-
${queueInfoParentTitle}
+
Artist / Show / Book
${queueItems}
@@ -205,11 +177,6 @@ export class PlayerBodyQueue extends PlayerBodyBase { this.updateActions(this.player, [Actions.GetPlayerQueueInfo]); - } else if (action == Actions.ChapterPlay) { - - await this.spotifyPlusService.Card_PlayMediaBrowserItem(this.player, item); - this.progressHide(); - } else if (action == Actions.EpisodePlay) { await this.spotifyPlusService.Card_PlayMediaBrowserItem(this.player, item); diff --git a/src/components/player-progress.ts b/src/components/player-progress.ts index fc7076f..49c1ddb 100644 --- a/src/components/player-progress.ts +++ b/src/components/player-progress.ts @@ -217,7 +217,7 @@ class Progress extends LitElement { width: 100%; font-size: x-small; display: flex; - color: var(--spc-player-controls-color, var(--spc-player-controls-icon-color, #ffffff)); + color: var(--spc-player-progress-label-color, var(--spc-player-controls-color, #ffffff)); padding-bottom: 0.2rem; } @@ -237,7 +237,7 @@ class Progress extends LitElement { .progress-bar { align-self: center; - background-color: var(--dark-primary-color); + background-color: var(--spc-player-progress-slider-color, var(--spc-player-controls-color, var(--dark-primary-color, #2196F3))); margin-left: 2px; margin-right: 2px; height: 50%; diff --git a/src/components/player-volume.ts b/src/components/player-volume.ts index 1922ee8..df311fd 100644 --- a/src/components/player-volume.ts +++ b/src/components/player-volume.ts @@ -344,7 +344,7 @@ class Volume extends LitElement { static get styles() { return css` ha-control-slider { - --control-slider-color: var(--dark-primary-color); + --control-slider-color: var(--spc-player-volume-slider-color, var(--spc-player-controls-color, var(--dark-primary-color, #2196F3))); --control-slider-thickness: 1rem; } @@ -362,7 +362,7 @@ class Volume extends LitElement { flex: 1; padding-right: 0.0rem; align-content: flex-end; - color: var(--spc-player-controls-color, var(--spc-player-controls-icon-color, #ffffff)); + color: var(--spc-player-volume-label-color, var(--spc-player-controls-color, #ffffff)); } .volume-level { @@ -376,7 +376,7 @@ class Volume extends LitElement { padding-right: 2px; font-weight: normal; font-size: 10px; - color: var(--dark-primary-color); + color: var(--spc-player-volume-slider-color, var(--spc-player-controls-color, var(--dark-primary-color, #2196F3))); } *[slim] * { diff --git a/src/constants.ts b/src/constants.ts index 9e7873c..304999f 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.37'; +export const CARD_VERSION = '1.0.38'; /** SpotifyPlus integration domain identifier. */ export const DOMAIN_SPOTIFYPLUS = 'spotifyplus'; @@ -49,7 +49,7 @@ export const PLAYER_CONTROLS_BACKGROUND_COLOR_DEFAULT = '#000000BB'; /** default size of the icons in the Player controls area. */ export const PLAYER_CONTROLS_ICON_SIZE_DEFAULT = '2.0rem'; -/** default size of the icons in the Player controls area. */ +/** default color of toggled icons in the Player controls area. */ export const PLAYER_CONTROLS_ICON_TOGGLE_COLOR_DEFAULT = '#2196F3'; /** default size of the player background image. */ diff --git a/src/editor/search-media-browser-editor.ts b/src/editor/search-media-browser-editor.ts index 2795e3f..5bf158c 100644 --- a/src/editor/search-media-browser-editor.ts +++ b/src/editor/search-media-browser-editor.ts @@ -76,12 +76,13 @@ const CONFIG_SETTINGS_SCHEMA = [ required: false, selector: { boolean: {} }, }, - //{ - // name: 'searchMediaBrowserItemsSortTitle', - // label: 'Sort items by Title', - // required: false, - // selector: { boolean: {} }, - //}, + { + name: 'searchMediaBrowserQueueSelection', + label: 'Queue track / episode when selected', + help: 'otherwise play immediately', + required: false, + selector: { boolean: {} }, + }, ]; diff --git a/src/sections/fav-browser-base.ts b/src/sections/fav-browser-base.ts index 0a937e8..e8aee77 100644 --- a/src/sections/fav-browser-base.ts +++ b/src/sections/fav-browser-base.ts @@ -639,6 +639,42 @@ export class FavBrowserBase extends LitElement { } + /** + * Calls the SpotifyPlusService Card_PlayMediaBrowserItem method to play media. + * + * @param mediaItem The medialist item that was selected. + */ + protected async QueueMediaItem(mediaItem: any): Promise { + + try { + + // show progress indicator. + this.progressShow(); + + // play media item. + await this.spotifyPlusService.AddPlayerQueueItems(this.player, mediaItem.uri); + + // set info message and reset scroll position to zero so the message is displayed. + this.alertInfo = "Item added to play queue: \"" + mediaItem.name + "\"."; + this.mediaBrowserContentElement.scrollTop = 0; + + } + catch (error) { + + // set error message and reset scroll position to zero so the message is displayed. + this.alertErrorSet("Could not play media item. " + getHomeAssistantErrorMessage(error)); + this.mediaBrowserContentElement.scrollTop = 0; + + } + finally { + + // hide progress indicator. + this.progressHide(); + + } + } + + /** * Sets the scroll position on the media list content container. */ diff --git a/src/sections/player.ts b/src/sections/player.ts index bc8b19c..0cdd2a4 100644 --- a/src/sections/player.ts +++ b/src/sections/player.ts @@ -146,7 +146,6 @@ export class Player extends LitElement implements playerAlerts { 'body' 'controls'; align-items: center; - /*background-color: #000000;*/ background-position: center; background-repeat: no-repeat; background-size: var(--spc-player-background-size, 100% 100%); /* PLAYER_BACKGROUND_IMAGE_SIZE_DEFAULT */ @@ -271,6 +270,10 @@ export class Player extends LitElement implements playerAlerts { const playerControlsIconColor = this.config.playerControlsIconColor; const playerControlsIconToggleColor = this.config.playerControlsIconToggleColor; const playerControlsColor = this.config.playerControlsColor; + const playerProgressSliderColor = this.config.playerProgressSliderColor; + const playerProgressLabelColor = this.config.playerProgressLabelColor; + const playerVolumeSliderColor = this.config.playerVolumeSliderColor; + const playerVolumeLabelColor = this.config.playerVolumeLabelColor; // build style info object. const styleInfo: StyleInfo = {}; @@ -287,6 +290,14 @@ export class Player extends LitElement implements playerAlerts { styleInfo['--spc-player-controls-icon-color'] = `${playerControlsIconColor}`; styleInfo['--spc-player-controls-icon-size'] = `${playerControlsIconSize}`; styleInfo['--spc-player-controls-icon-button-size'] = `var(--spc-player-controls-icon-size, ${PLAYER_CONTROLS_ICON_SIZE_DEFAULT}) + 0.75rem`; + if (playerProgressLabelColor) + styleInfo['--spc-player-progress-label-color'] = `${playerProgressLabelColor}`; + if (playerProgressSliderColor) + styleInfo['--spc-player-progress-slider-color'] = `${playerProgressSliderColor}`; + if (playerVolumeLabelColor) + styleInfo['--spc-player-volume-label-color'] = `${playerVolumeLabelColor}`; + if (playerVolumeSliderColor) + styleInfo['--spc-player-volume-slider-color'] = `${playerVolumeSliderColor}`; styleInfo['--spc-player-palette-vibrant'] = `${this._colorPaletteVibrant}`; styleInfo['--spc-player-palette-muted'] = `${this._colorPaletteMuted}`; styleInfo['--spc-player-palette-darkvibrant'] = `${this._colorPaletteDarkVibrant}`; diff --git a/src/sections/search-media-browser.ts b/src/sections/search-media-browser.ts index a1f5a45..b598b11 100644 --- a/src/sections/search-media-browser.ts +++ b/src/sections/search-media-browser.ts @@ -395,6 +395,35 @@ export class SearchBrowser extends FavBrowserBase { } + /** + * Handles the `item-selected` event fired when a media browser item is clicked. + * + * @param args Event arguments that contain the media item that was clicked on. + */ + protected override onItemSelected(args: CustomEvent) { + + if (debuglog.enabled) { + debuglog("onItemSelected - search media item selected:\n%s", + JSON.stringify(args.detail, null, 2), + ); + } + + const mediaItem = args.detail; + + // if media item is a track or episode, then queue the item if configured to do so. + if (this.config.searchMediaBrowserQueueSelection) { + if ((mediaItem.type == 'track') || (mediaItem.type == 'episode')) { + this.QueueMediaItem(mediaItem); + return; + } + } + + // otherwise, just play the media item. + this.PlayMediaItem(mediaItem); + + } + + /** * Returns false if the specified feature is to be SHOWN; otherwise, returns true * if the specified feature is to be HIDDEN (via CSS). diff --git a/src/types/card-config.ts b/src/types/card-config.ts index 0588340..907950f 100644 --- a/src/types/card-config.ts +++ b/src/types/card-config.ts @@ -59,6 +59,12 @@ export interface CardConfig extends LovelaceCardConfig { */ touchSupportDisabled?: boolean; + /** + * Color of the card area wait progress indicator. + * Default is '#2196F3'. + */ + cardWaitProgressSliderColor?: string; + /** * Title displayed at the top of the Album Favorites media browser section form. * Omit this parameter to hide the title display area. @@ -469,6 +475,18 @@ export interface CardConfig extends LovelaceCardConfig { */ playerControlsIconToggleColor?: string; + /** + * Color of the player progress text labels. + * Default is '#ffffff'. + */ + playerProgressLabelColor?: string; + + /** + * Color of the player progress slider bar. + * Default is '#2196F3'. + */ + playerProgressSliderColor?: string; + /** * Hide volume level numbers and percentages in the volume controls area of the Player * section form. Volume slider control is not affected by this setting. @@ -501,6 +519,18 @@ export interface CardConfig extends LovelaceCardConfig { */ playerVolumeMaxValue?: number; + /** + * Color of the player volume text labels. + * Default is '#ffffff'. + */ + playerVolumeLabelColor?: string; + + /** + * Color of the player volume slider bar. + * Default is '#2196F3'. + */ + playerVolumeSliderColor?: string; + /** * Title displayed at the top of the Playlist Favorites media browser section form. * Omit this parameter to hide the title display area. @@ -621,6 +651,13 @@ export interface CardConfig extends LovelaceCardConfig { */ searchMediaBrowserItemsHideSubTitle?: boolean; + /** + * True to add track / episode to play queue when search result is clicked; + * Otherwise, play the track / episode immediately when search result is clicked. + * Default is false. + */ + searchMediaBrowserQueueSelection?: boolean; + /** * Maximum number of items to be returned by the search via the Search media browser section form. * Default is 50.