Skip to content

Commit

Permalink
feature: make it possible to view and manage the queue (aka playlist)
Browse files Browse the repository at this point in the history
  • Loading branch information
punxaphil committed Nov 7, 2024
1 parent 7875fbd commit 48fe406
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 64 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Sonos Card for Home Assistant UI with a focus on managing multiple media players
* Configurable styling
* Dynamic volume level slider
* Track progress bar
* Show, play and remove tracks of play queue

and more!

Expand All @@ -24,6 +25,7 @@ and more!
![groups.png](https://github.com/punxaphil/custom-sonos-card/raw/main/img/groups.png)
![grouping.png](https://github.com/punxaphil/custom-sonos-card/raw/main/img/grouping.png)
![volumes.png](https://github.com/punxaphil/custom-sonos-card/raw/main/img/volumes.png)
![queue.png](https://github.com/punxaphil/custom-sonos-card/raw/main/img/queue.png)

## Support the project

Expand Down Expand Up @@ -112,6 +114,7 @@ sections: # see explanation further up
- grouping
- media browser
- player
- queue
widthPercentage: 75 # default is 100. Use this to change the width of the card.
heightPercentage: 75 # default is 100. Use this to change the height of the card. Set to 'auto' to make the card height adjust to the content.
entityId: media_player.bedroom # Forces this player to be the selected one on loading the card (overrides url param etc)
Expand Down Expand Up @@ -237,6 +240,9 @@ mediaBrowserTitle: My favorites # default is 'All favorites'. Use this to change

# volumes specific
hideVolumeCogwheel: true # default is false. Will hide the cogwheel for the volumes section.

# queue specific
queueTitle: Songs # default is 'Play Queue'. Use this to change the title for the queue section.
```
## Using individual section cards
Expand Down
Binary file removed img/card_mod.png
Binary file not shown.
Binary file added img/queue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions src/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { when } from 'lit/directives/when.js';
import { styleMap } from 'lit-html/directives/style-map.js';
import { cardDoesNotContainAllSections, getHeight, getWidth, isSonosCard } from './utils/utils';

const { GROUPING, GROUPS, MEDIA_BROWSER, PLAYER, VOLUMES } = Section;
const { GROUPING, GROUPS, MEDIA_BROWSER, PLAYER, VOLUMES, QUEUE } = Section;
const TITLE_HEIGHT = 2;
const FOOTER_HEIGHT = 5;

Expand Down Expand Up @@ -71,6 +71,11 @@ export class Card extends LitElement {
`,
],
[VOLUMES, () => html` <sonos-volumes .store=${this.store}></sonos-volumes>`],
[
QUEUE,
() =>
html`<sonos-queue .store=${this.store} @item-selected=${this.onMediaItemSelected}></sonos-queue>`,
],
])
: html`<div class="no-players">No supported players found</div>`
}
Expand Down Expand Up @@ -181,7 +186,7 @@ export class Card extends LitElement {
footerStyle() {
return styleMap({
height: `${FOOTER_HEIGHT}rem`,
paddingBottom: '1rem',
padding: '0 1rem',
});
}

Expand Down Expand Up @@ -209,10 +214,13 @@ export class Card extends LitElement {
? GROUPS
: sections.includes(GROUPING)
? GROUPING
: VOLUMES;
: sections.includes(QUEUE)
? QUEUE
: VOLUMES;
} else {
this.section = PLAYER;
}

newConfig.mediaBrowserItemsPerRow = newConfig.mediaBrowserItemsPerRow || 4;
// support custom:auto-entities
if (newConfig.entities?.length && newConfig.entities[0].entity) {
Expand Down
5 changes: 3 additions & 2 deletions src/components/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { property } from 'lit/decorators.js';
import { CardConfig, Section } from '../types';
import './section-button';

const { GROUPING, GROUPS, MEDIA_BROWSER, PLAYER, VOLUMES } = Section;
const { GROUPING, GROUPS, MEDIA_BROWSER, PLAYER, VOLUMES, QUEUE } = Section;

class Footer extends LitElement {
@property({ attribute: false }) config!: CardConfig;
Expand All @@ -16,6 +16,7 @@ class Footer extends LitElement {
[MEDIA_BROWSER, icons?.mediaBrowser ?? 'mdi:star-outline'],
[GROUPS, icons?.groups ?? 'mdi:speaker-multiple'],
[GROUPING, icons?.grouping ?? 'mdi:checkbox-multiple-marked-circle-outline'],
[QUEUE, icons?.queue ?? 'mdi:queue-first-in-last-out'],
[VOLUMES, icons?.volumes ?? 'mdi:tune'],
];
sections = sections.filter(([section]) => !this.config.sections || this.config.sections?.includes(section));
Expand All @@ -40,7 +41,7 @@ class Footer extends LitElement {
justify-content: space-between;
}
:host > * {
padding: 1rem;
padding: 1rem 0;
}
`;
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/media-browser-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { property } from 'lit/decorators.js';
import Store from '../model/store';
import { CardConfig, MediaPlayerItem } from '../types';
import { customEvent } from '../utils/utils';
import { MEDIA_ITEM_SELECTED, mediaBrowserTitleStyle } from '../constants';
import { MEDIA_ITEM_SELECTED, mediaItemTitleStyle } from '../constants';
import { itemsWithFallbacks, renderMediaBrowserItem } from '../utils/media-browser-utils';
import { styleMap } from 'lit-html/directives/style-map.js';

Expand Down Expand Up @@ -43,7 +43,7 @@ export class MediaBrowserIcons extends LitElement {

static get styles() {
return [
mediaBrowserTitleStyle,
mediaItemTitleStyle,
css`
.icons {
display: flex;
Expand Down
42 changes: 8 additions & 34 deletions src/components/media-browser-list.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { css, html, LitElement } from 'lit';
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import Store from '../model/store';
import { CardConfig, MediaPlayerItem } from '../types';
import { customEvent } from '../utils/utils';
import { listStyle, MEDIA_ITEM_SELECTED, mediaBrowserTitleStyle } from '../constants';
import { itemsWithFallbacks, renderMediaBrowserItem } from '../utils/media-browser-utils';
import { listStyle, MEDIA_ITEM_SELECTED } from '../constants';
import { itemsWithFallbacks } from '../utils/media-browser-utils';

export class MediaBrowserList extends LitElement {
@property({ attribute: false }) store!: Store;
Expand All @@ -18,44 +18,18 @@ export class MediaBrowserList extends LitElement {
<mwc-list multi class="list">
${itemsWithFallbacks(this.items, this.config).map((item) => {
return html`
<mwc-list-item class="button" @click=${() => this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, item))}>
<div class="row">${renderMediaBrowserItem(item)}</div>
</mwc-list-item>
<sonos-media-row
@click=${() => this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, item))}
.item=${item}
></sonos-media-row>
`;
})}
</mwc-list>
`;
}

static get styles() {
return [
css`
.button {
--icon-width: 35px;
height: 40px;
}
.row {
display: flex;
}
.thumbnail {
width: var(--icon-width);
height: var(--icon-width);
background-size: contain;
background-repeat: no-repeat;
background-position: left;
}
.title {
font-size: 1.1rem;
align-self: center;
flex: 1;
}
`,
mediaBrowserTitleStyle,
listStyle,
];
return listStyle;
}
}

Expand Down
76 changes: 76 additions & 0 deletions src/components/media-row.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { css, html, LitElement, PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import Store from '../model/store';
import { MediaPlayerItem } from '../types';
import { mediaItemTitleStyle } from '../constants';
import { renderMediaBrowserItem } from '../utils/media-browser-utils';

class MediaRow extends LitElement {
@property({ attribute: false }) store!: Store;
@property({ attribute: false }) item!: MediaPlayerItem;
@property({ type: Boolean }) selected = false;

render() {
return html`
<mwc-list-item hasMeta ?selected=${this.selected} ?activated=${this.selected} class="button">
<div class="row">${renderMediaBrowserItem(this.item)}</div>
<slot slot="meta"></slot>
</mwc-list-item>
`;
}

protected async firstUpdated(_changedProperties: PropertyValues) {
super.firstUpdated(_changedProperties);
await this.scrollToSelected(_changedProperties);
}

protected async updated(_changedProperties: PropertyValues) {
super.updated(_changedProperties);
await this.scrollToSelected(_changedProperties);
}

private async scrollToSelected(_changedProperties: PropertyValues) {
await new Promise((r) => setTimeout(r, 0));
if (this.selected && _changedProperties.has('selected')) {
this.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}

static get styles() {
return [
css`
.mdc-deprecated-list-item__text {
width: 100%;
}
.button {
margin: 0.3rem;
border-radius: 0.3rem;
background: var(--secondary-background-color);
--icon-width: 35px;
height: 40px;
}
.row {
display: flex;
}
.thumbnail {
width: var(--icon-width);
height: var(--icon-width);
background-size: contain;
background-repeat: no-repeat;
background-position: left;
}
.title {
font-size: 1.1rem;
align-self: center;
flex: 1;
}
`,
mediaItemTitleStyle,
];
}
}

customElements.define('sonos-media-row', MediaRow);
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const listStyle = css`
}
`;

export const mediaBrowserTitleStyle = css`
export const mediaItemTitleStyle = css`
.title {
color: var(--secondary-text-color);
font-weight: bold;
Expand Down
4 changes: 4 additions & 0 deletions src/editor/advanced-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const ADVANCED_SCHEMA = [
name: 'mediaBrowserTitle',
type: 'string',
},
{
name: 'queueTitle',
type: 'string',
},
{
name: 'artworkHostname',
type: 'string',
Expand Down
9 changes: 8 additions & 1 deletion src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ class CardEditor extends BaseEditor {

protected render(): TemplateResult {
if (!this.config.sections || this.config.sections.length === 0) {
this.config.sections = [Section.PLAYER, Section.VOLUMES, Section.GROUPS, Section.GROUPING, Section.MEDIA_BROWSER];
this.config.sections = [
Section.PLAYER,
Section.VOLUMES,
Section.GROUPS,
Section.GROUPING,
Section.MEDIA_BROWSER,
Section.QUEUE,
];
}

return html`
Expand Down
16 changes: 9 additions & 7 deletions src/editor/general-editor.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { html, TemplateResult } from 'lit';
import { BaseEditor } from './base-editor';

const options = {
player: 'Player',
'media browser': 'Media Browser',
groups: 'Groups',
grouping: 'Grouping',
volumes: 'Volumes',
queue: 'Queue',
};
export const GENERAL_SCHEMA = [
{
type: 'multi_select',
options: {
player: 'Player',
'media browser': 'Media Browser',
groups: 'Groups',
grouping: 'Grouping',
volumes: 'Volumes',
},
options: options,
name: 'sections',
},
{
Expand Down
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Groups } from './sections/groups';
import { MediaBrowser } from './sections/media-browser';
import './sections/volumes';
import './components/ha-player';
import { Queue } from './sections/queue';
import { Volumes } from './sections/volumes';

window.customCards.push({
type: 'sonos-card',
Expand All @@ -18,3 +20,5 @@ customElements.define('sonos-grouping', Grouping);
customElements.define('sonos-groups', Groups);
customElements.define('sonos-media-browser', MediaBrowser);
customElements.define('sonos-player', Player);
customElements.define('sonos-volumes', Volumes);
customElements.define('sonos-queue', Queue);
Loading

0 comments on commit 48fe406

Please sign in to comment.