diff --git a/src/components/media-browser-header.ts b/src/components/media-browser-header.ts index 5c5027c4..3c81e6d9 100644 --- a/src/components/media-browser-header.ts +++ b/src/components/media-browser-header.ts @@ -1,59 +1,36 @@ import { css, html, LitElement } from 'lit'; -import { property, state } from 'lit/decorators.js'; -import { CardConfig } from '../types'; -import { mdiArrowUpLeftBold, mdiPlay, mdiPlayBoxMultiple, mdiStarOutline } from '@mdi/js'; -import { BROWSE_CLICKED, BROWSE_STATE, PLAY_DIR } from '../constants'; -import { iconButton } from './icon-button'; +import { property } from 'lit/decorators.js'; +import { MediaPlayerEntityFeature } from '../types'; +import Store from '../model/store'; +import { styleMap } from 'lit-html/directives/style-map.js'; class MediaBrowserHeader extends LitElement { - @property() config!: CardConfig; - @state() browseCanPlay!: boolean; - @state() browseMedia = true; - @state() mediaBrowserDir!: string; - @state() title!: string; + @property() store!: Store; render() { - const browseIcon = this.browseMedia - ? mdiPlayBoxMultiple - : this.mediaBrowserDir - ? mdiArrowUpLeftBold - : mdiStarOutline; + const state = this.store.hass.states[this.store.activePlayer.id]; + const playerState = { + ...state, + attributes: { ...state.attributes, supported_features: MediaPlayerEntityFeature.BROWSE_MEDIA }, + }; return html` -
- ${this.browseCanPlay - ? iconButton(mdiPlay, () => window.dispatchEvent(new CustomEvent(PLAY_DIR)), { - additionalStyle: { padding: '0.5rem' }, - }) - : ''} -
-
${this.title}
- ${iconButton(browseIcon, () => window.dispatchEvent(new CustomEvent(BROWSE_CLICKED)), { - additionalStyle: { padding: '0.5rem', flex: '1', textAlign: 'right' }, - })} +
All Favorites
+ `; } - connectedCallback() { - super.connectedCallback(); - window.addEventListener(BROWSE_STATE, (event: Event) => { - const detail = (event as CustomEvent).detail; - this.browseCanPlay = detail.canPlay; - this.browseMedia = !detail.browse; - this.mediaBrowserDir = detail.currentDir; - this.title = detail.title; - }); - } static get styles() { return css` :host { display: flex; justify-content: space-between; } - .play { - flex: 1; - } .title { - flex: 6; + flex: 1; text-align: center; font-size: 1.2rem; font-weight: bold; diff --git a/src/components/media-browser-icons.ts b/src/components/media-browser-icons.ts index 75604e53..d37eb08c 100755 --- a/src/components/media-browser-icons.ts +++ b/src/components/media-browser-icons.ts @@ -26,13 +26,12 @@ export class MediaBrowserIcons extends LitElement {
${itemsWithFallbacks(this.items, this.config).map( - (item, index) => - html` - ${mediaItemBackgroundImageStyle(item.thumbnail, index)} - - ${renderMediaBrowserItem(item, !item.thumbnail || !!this.config.mediaBrowserShowTitleForThumbnailIcons)} - - `, + (item, index) => html` + ${mediaItemBackgroundImageStyle(item.thumbnail, index)} + + ${renderMediaBrowserItem(item, !item.thumbnail || !!this.config.mediaBrowserShowTitleForThumbnailIcons)} + + `, )}
`; @@ -63,11 +62,6 @@ export class MediaBrowserIcons extends LitElement { background-position: center; } - .folder { - margin: 5% 15% 25% 15%; - --mdc-icon-size: 100%; - } - .title { font-size: 0.8rem; position: absolute; diff --git a/src/components/media-browser-list.ts b/src/components/media-browser-list.ts index 69a816f8..bb78cbb5 100755 --- a/src/components/media-browser-list.ts +++ b/src/components/media-browser-list.ts @@ -52,11 +52,6 @@ export class MediaBrowserList extends LitElement { background-position: left; } - .folder { - width: var(--icon-width); - --mdc-icon-size: 100%; - } - .title { font-size: 1.1rem; align-self: center; diff --git a/src/constants.ts b/src/constants.ts index fb163cbd..6fc34878 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,9 +6,6 @@ export const REQUEST_PLAYER_EVENT = dispatchPrefix + 'request-player'; export const SHOW_SECTION = dispatchPrefix + 'show-section'; export const CALL_MEDIA_STARTED = dispatchPrefix + 'call-media-started'; export const CALL_MEDIA_DONE = dispatchPrefix + 'call-media-done'; -export const PLAY_DIR = dispatchPrefix + 'play-dir'; -export const BROWSE_CLICKED = dispatchPrefix + 'browse-up'; -export const BROWSE_STATE = dispatchPrefix + 'browse-state'; export const MEDIA_ITEM_SELECTED = dispatchPrefix + 'media-item-selected'; export const TV_BASE64_IMAGE = diff --git a/src/model/media-player.ts b/src/model/media-player.ts index c0f26354..008569ce 100644 --- a/src/model/media-player.ts +++ b/src/model/media-player.ts @@ -7,9 +7,7 @@ export class MediaPlayer { name: string; state: string; members: MediaPlayer[]; - attributes: { - [key: string]: any; - }; + attributes: HassEntity['attributes']; private readonly config: CardConfig; constructor(hassEntity: HassEntity, config: CardConfig, mediaPlayerHassEntities?: HassEntity[]) { @@ -21,9 +19,6 @@ export class MediaPlayer { this.members = mediaPlayerHassEntities ? this.createGroupMembers(hassEntity, mediaPlayerHassEntities) : []; } - hasPlayer(playerId: string) { - return this.getPlayer(playerId) !== undefined; - } getPlayer(playerId: string) { return this.id === playerId ? this : this.getMember(playerId); } diff --git a/src/sections/media-browser.ts b/src/sections/media-browser.ts index b430a95b..307c63bd 100755 --- a/src/sections/media-browser.ts +++ b/src/sections/media-browser.ts @@ -1,50 +1,32 @@ import { html, LitElement } from 'lit'; -import { until } from 'lit-html/directives/until.js'; -import { property, state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import '../components/media-browser-list'; import '../components/media-browser-icons'; import '../components/media-browser-header'; -import MediaBrowseService from '../services/media-browse-service'; import MediaControlService from '../services/media-control-service'; import Store from '../model/store'; import { CardConfig, MediaPlayerItem, Section } from '../types'; import { dispatchShowSection } from '../utils/utils'; -import { BROWSE_CLICKED, BROWSE_STATE, MEDIA_ITEM_SELECTED, PLAY_DIR } from '../constants'; +import { MEDIA_ITEM_SELECTED } from '../constants'; import { MediaPlayer } from '../model/media-player'; +import { until } from 'lit-html/directives/until.js'; +import MediaBrowseService from '../services/media-browse-service'; import { indexOfWithoutSpecialChars } from '../utils/media-browser-utils'; -const LOCAL_STORAGE_CURRENT_DIR = 'custom-sonos-card_currentDir'; -const LOCAL_STORAGE_BROWSE = 'custom-sonos-card_browse'; - export class MediaBrowser extends LitElement { @property() store!: Store; private config!: CardConfig; private activePlayer!: MediaPlayer; - @state() private browse!: boolean; - @state() private currentDir?: MediaPlayerItem; private mediaPlayers!: MediaPlayer[]; - private parentDirs: MediaPlayerItem[] = []; private mediaControlService!: MediaControlService; private mediaBrowseService!: MediaBrowseService; - private readonly playDirListener = async () => { - await this.playItem(this.currentDir); - }; - - private readonly browseClickedListener = async () => { - this.browseClicked(); - }; - connectedCallback() { super.connectedCallback(); - window.addEventListener(PLAY_DIR, this.playDirListener); - window.addEventListener(BROWSE_CLICKED, this.browseClickedListener); window.addEventListener(MEDIA_ITEM_SELECTED, this.onMediaItemSelected); } disconnectedCallback() { - window.removeEventListener(PLAY_DIR, this.playDirListener); - window.removeEventListener(BROWSE_CLICKED, this.browseClickedListener); window.removeEventListener(MEDIA_ITEM_SELECTED, this.onMediaItemSelected); super.disconnectedCallback(); } @@ -56,23 +38,13 @@ export class MediaBrowser extends LitElement { this.mediaPlayers = this.store.allMediaPlayers; this.mediaControlService = this.store.mediaControlService; - const currentDirJson = localStorage.getItem(LOCAL_STORAGE_CURRENT_DIR); - if (currentDirJson) { - const currentDir = JSON.parse(currentDirJson); - if (currentDir !== this.currentDir) { - this.currentDir = currentDir; - this.browse = true; - this.dispatchBrowseState(); - } - } else { - this.browse = !!localStorage.getItem(LOCAL_STORAGE_BROWSE); - } return html` - + + ${this.activePlayer && until( - (this.browse ? this.loadMediaDir(this.currentDir) : this.getAllFavorites()).then((items) => { - return this.config.mediaBrowserItemsPerRow > 1 && this.currentDir?.children_media_class !== 'track' + this.getAllFavorites().then((items) => { + return this.config.mediaBrowserItemsPerRow > 1 ? html`` : html` `; }), @@ -80,60 +52,10 @@ export class MediaBrowser extends LitElement { `; } - firstUpdated() { - this.dispatchBrowseState(); - } - - private dispatchBrowseState() { - const title = !this.browse ? 'All Favorites' : this.currentDir ? this.currentDir.title : 'Media Browser'; - window.dispatchEvent( - new CustomEvent(BROWSE_STATE, { - detail: { - canPlay: this.currentDir?.can_play, - browse: this.browse, - currentDir: this.currentDir, - title, - }, - }), - ); - } - - private browseClicked() { - if (this.parentDirs.length) { - this.setCurrentDir(this.parentDirs.pop()); - } else if (this.currentDir) { - this.setCurrentDir(undefined); - } else { - this.browse = !this.browse; - - if (this.browse) { - localStorage.setItem(LOCAL_STORAGE_BROWSE, 'true'); - } else { - localStorage.removeItem(LOCAL_STORAGE_BROWSE); - } - this.dispatchBrowseState(); - } - } - - private setCurrentDir(mediaItem?: MediaPlayerItem) { - this.currentDir = mediaItem; - if (mediaItem) { - localStorage.setItem(LOCAL_STORAGE_CURRENT_DIR, JSON.stringify(mediaItem)); - } else { - localStorage.removeItem(LOCAL_STORAGE_CURRENT_DIR); - } - this.dispatchBrowseState(); - } - private onMediaItemSelected = (event: Event) => { const mediaItem = (event as CustomEvent).detail; - if (mediaItem.can_expand) { - this.currentDir && this.parentDirs.push(this.currentDir); - this.setCurrentDir(mediaItem); - } else if (mediaItem.can_play) { - this.playItem(mediaItem); - setTimeout(() => dispatchShowSection(Section.PLAYER), 1000); - } + this.playItem(mediaItem); + setTimeout(() => dispatchShowSection(Section.PLAYER), 1000); }; private async playItem(mediaItem: MediaPlayerItem) { @@ -179,10 +101,4 @@ export class MediaBrowser extends LitElement { private static createSource(source: MediaPlayerItem) { return { ...source, can_play: true }; } - - private async loadMediaDir(mediaItem?: MediaPlayerItem) { - return await (mediaItem - ? this.mediaBrowseService.getDir(this.activePlayer, mediaItem, this.config.mediaBrowserTitlesToIgnore) - : this.mediaBrowseService.getRoot(this.activePlayer, this.config.mediaBrowserTitlesToIgnore)); - } } diff --git a/src/services/media-browse-service.ts b/src/services/media-browse-service.ts index 9c926e92..3f3981e4 100644 --- a/src/services/media-browse-service.ts +++ b/src/services/media-browse-service.ts @@ -3,14 +3,6 @@ import HassService from './hass-service'; import { MediaPlayer } from '../model/media-player'; import { indexOfWithoutSpecialChars } from '../utils/media-browser-utils'; -function mediaBrowserFilter(ignoredTitles: string[] = [], items?: MediaPlayerItem[]) { - return items?.filter( - (item) => - !['media-source://tts', 'media-source://camera'].includes(item.media_content_id || '') && - indexOfWithoutSpecialChars(ignoredTitles, item.title) === -1, - ); -} - export default class MediaBrowseService { private hassService: HassService; @@ -18,31 +10,15 @@ export default class MediaBrowseService { this.hassService = hassService; } - async getRoot(mediaPlayer: MediaPlayer, ignoredTitles?: string[]): Promise { - const root = await this.hassService.browseMedia(mediaPlayer); - return mediaBrowserFilter(ignoredTitles, root.children) || []; - } - - async getDir(mediaPlayer: MediaPlayer, dir: MediaPlayerItem, ignoredTitles?: string[]): Promise { - try { - const dirItem = await this.hassService.browseMedia(mediaPlayer, dir.media_content_type, dir.media_content_id); - return mediaBrowserFilter(ignoredTitles, dirItem.children) || []; - } catch (e) { - console.error(e); - return []; - } - } - async getAllFavorites(mediaPlayers: MediaPlayer[], ignoredTitles?: string[]): Promise { if (!mediaPlayers.length) { return []; } - const favoritesForAllPlayers = await Promise.all( - mediaPlayers.map((player) => this.getFavoritesForPlayer(player, ignoredTitles)), - ); + const favoritesForAllPlayers = await Promise.all(mediaPlayers.map((player) => this.getFavoritesForPlayer(player))); let favorites = favoritesForAllPlayers.flatMap((f) => f); favorites = this.removeDuplicates(favorites); - return favorites.length ? favorites : this.getFavoritesFromStates(mediaPlayers); + favorites = favorites.length ? favorites : this.getFavoritesFromStates(mediaPlayers); + return favorites.filter((item) => indexOfWithoutSpecialChars(ignoredTitles ?? [], item.title) === -1); } private removeDuplicates(items: MediaPlayerItem[]) { @@ -51,13 +27,13 @@ export default class MediaBrowseService { }); } - private async getFavoritesForPlayer(player: MediaPlayer, ignoredTitles?: string[]) { + private async getFavoritesForPlayer(player: MediaPlayer) { const favoritesRoot = await this.hassService.browseMedia(player, 'favorites', ''); const favoriteTypesPromise = favoritesRoot.children?.map((favoriteItem) => this.hassService.browseMedia(player, favoriteItem.media_content_type, favoriteItem.media_content_id), ); const favoriteTypes = favoriteTypesPromise ? await Promise.all(favoriteTypesPromise) : []; - return favoriteTypes.flatMap((item) => mediaBrowserFilter(ignoredTitles, item.children) || []); + return favoriteTypes.flatMap((item) => item.children ?? []); } private getFavoritesFromStates(mediaPlayers: MediaPlayer[]) { diff --git a/src/types.ts b/src/types.ts index 54fefd17..625f18dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,8 +36,8 @@ export interface CardConfig extends LovelaceCardConfig { dynamicVolumeSlider: boolean; mediaArtworkOverrides?: MediaArtworkOverride[]; customSources?: CustomSources; - customThumbnail?: CustomThumbnail; - customThumbnailIfMissing?: CustomThumbnail; + customThumbnail?: CustomThumbnails; + customThumbnailIfMissing?: CustomThumbnails; mediaBrowserTitlesToIgnore?: string[]; mediaBrowserItemsPerRow: number; mediaBrowserShowTitleForThumbnailIcons?: boolean; @@ -62,7 +62,7 @@ export interface CustomSource { thumbnail?: string; } -export interface CustomThumbnail { +export interface CustomThumbnails { [title: string]: string; } @@ -72,11 +72,8 @@ export interface MediaPlayerItem { children?: MediaPlayerItem[]; children_media_class?: string; media_class?: string; - can_expand?: boolean; - can_play?: boolean; media_content_type?: string; media_content_id?: string; - showFolderIcon?: boolean; } export interface PredefinedGroup { @@ -95,3 +92,26 @@ export interface PredefinedGroupPlayer { export interface TemplateResult { result: string[]; } + +export const enum MediaPlayerEntityFeature { + PAUSE = 1, + SEEK = 2, + VOLUME_SET = 4, + VOLUME_MUTE = 8, + PREVIOUS_TRACK = 16, + NEXT_TRACK = 32, + + TURN_ON = 128, + TURN_OFF = 256, + PLAY_MEDIA = 512, + VOLUME_BUTTONS = 1024, + SELECT_SOURCE = 2048, + STOP = 4096, + CLEAR_PLAYLIST = 8192, + PLAY = 16384, + SHUFFLE_SET = 32768, + SELECT_SOUND_MODE = 65536, + BROWSE_MEDIA = 131072, + REPEAT_SET = 262144, + GROUPING = 524288, +} diff --git a/src/utils/media-browser-utils.ts b/src/utils/media-browser-utils.ts index 8f087ec3..60e78ef7 100644 --- a/src/utils/media-browser-utils.ts +++ b/src/utils/media-browser-utils.ts @@ -1,4 +1,4 @@ -import { CardConfig, MediaPlayerItem } from '../types'; +import { CardConfig, CustomThumbnails, MediaPlayerItem } from '../types'; import { html } from 'lit'; const DEFAULT_MEDIA_THUMBNAIL = @@ -8,10 +8,10 @@ function hasItemsWithImage(items: MediaPlayerItem[]) { return items.some((item) => item.thumbnail); } -function getValueFromKeyIgnoreSpecialChars(array: { [title: string]: string } | undefined, str: string) { - for (const key in array) { - if (removeSpecialChars(key) === removeSpecialChars(str)) { - return array[key]; +function getValueFromKeyIgnoreSpecialChars(customThumbnails: CustomThumbnails | undefined, currentTitle: string) { + for (const title in customThumbnails) { + if (removeSpecialChars(title) === removeSpecialChars(currentTitle)) { + return customThumbnails[title]; } } return undefined; @@ -51,7 +51,6 @@ export function itemsWithFallbacks(mediaPlayerItems: MediaPlayerItem[], config: return { ...item, thumbnail, - showFolderIcon: item.can_expand && !thumbnail, }; }); } @@ -69,7 +68,6 @@ export function mediaItemBackgroundImageStyle(thumbnail: string, index: number) export function renderMediaBrowserItem(item: MediaPlayerItem, showTitle = true) { return html`
-
${item.title}
`; }