Skip to content

Commit b8ac43d

Browse files
committed
feat: implement inline play menu for media actions and enhance selection functionality
1 parent 2b70756 commit b8ac43d

File tree

7 files changed

+585
-198
lines changed

7 files changed

+585
-198
lines changed

src/components/media-row.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,24 @@ class MediaRow extends LitElement {
4747
style="${cssVars}"
4848
>
4949
<div class="row">
50-
<div class="icon-slot">
51-
${this.showCheckbox
52-
? html`<ha-checkbox
50+
${this.showCheckbox
51+
? html`<div class="icon-slot">
52+
<ha-checkbox
5353
.checked=${this.checked}
5454
@change=${this.onCheckboxChange}
5555
@click=${(e: Event) => e.stopPropagation()}
56-
></ha-checkbox>`
57-
: this.showQueueButton
58-
? html`<ha-icon-button
56+
></ha-checkbox>
57+
</div>`
58+
: this.showQueueButton
59+
? html`<div class="icon-slot">
60+
<ha-icon-button
5961
class=${classMap({ 'queue-btn': true, disabled: this.queueButtonDisabled })}
6062
.path=${mdiSkipNext}
6163
?disabled=${this.queueButtonDisabled}
6264
@click=${this.onQueueClick}
63-
></ha-icon-button>`
64-
: nothing}
65-
</div>
65+
></ha-icon-button>
66+
</div>`
67+
: nothing}
6668
${renderFavoritesItem(this.item)}
6769
</div>
6870
<div class="meta-content" slot="meta">
@@ -204,8 +206,8 @@ class MediaRow extends LitElement {
204206
}
205207
206208
.thumbnail {
207-
width: var(--icon-width);
208-
height: var(--icon-width);
209+
width: var(--icon-width, 20px);
210+
height: var(--icon-width, 20px);
209211
background-size: contain;
210212
background-repeat: no-repeat;
211213
background-position: left;

src/components/play-menu.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { css, html, LitElement, nothing } from 'lit';
2+
import { property } from 'lit/decorators.js';
3+
import {
4+
mdiAccessPoint,
5+
mdiClose,
6+
mdiPlay,
7+
mdiPlayBoxMultiple,
8+
mdiPlaylistPlus,
9+
mdiSkipNext,
10+
mdiSkipNextCircle,
11+
} from '@mdi/js';
12+
import { customEvent } from '../utils/utils';
13+
14+
export type PlayMenuAction = {
15+
enqueue: 'replace' | 'play' | 'next' | 'add' | 'replace_next';
16+
radioMode?: boolean;
17+
};
18+
19+
const PLAY_MENU_ACTIONS: PlayMenuAction[] = [
20+
{ enqueue: 'replace' },
21+
{ enqueue: 'play', radioMode: true },
22+
{ enqueue: 'play' },
23+
{ enqueue: 'next' },
24+
{ enqueue: 'add' },
25+
{ enqueue: 'replace_next' },
26+
];
27+
28+
export class PlayMenu extends LitElement {
29+
@property({ type: Boolean }) disabled = false;
30+
@property({ type: Boolean }) hasSelection = false;
31+
/** When true, renders as an already-open dropdown panel instead of a trigger button */
32+
@property({ type: Boolean }) inline = false;
33+
34+
render() {
35+
if (!this.hasSelection) {
36+
return nothing;
37+
}
38+
if (this.inline) {
39+
return this.renderInlineMenu();
40+
}
41+
return this.renderButtonMenu();
42+
}
43+
44+
private renderButtonMenu() {
45+
return html`
46+
<ha-button-menu fixed corner="BOTTOM_END" @action=${this.handleAction}>
47+
<ha-icon-button
48+
slot="trigger"
49+
.path=${mdiPlay}
50+
title="Play options"
51+
?disabled=${this.disabled}
52+
></ha-icon-button>
53+
${this.renderMenuItems()}
54+
</ha-button-menu>
55+
`;
56+
}
57+
58+
private renderInlineMenu() {
59+
return html`
60+
<div class="inline-menu" @click=${(e: Event) => e.stopPropagation()}>
61+
<ha-icon-button class="close-btn" .path=${mdiClose} @click=${this.closeMenu} title="Close"></ha-icon-button>
62+
${PLAY_MENU_ACTIONS.map(
63+
(_action, index) => html`
64+
<div class="inline-menu-item" @click=${() => this.selectAction(index)}>
65+
<ha-svg-icon .path=${this.getActionIcon(index)}></ha-svg-icon>
66+
<span>${this.getActionLabel(index)}</span>
67+
</div>
68+
`,
69+
)}
70+
</div>
71+
`;
72+
}
73+
74+
private renderMenuItems() {
75+
return html`
76+
<ha-list-item graphic="icon">
77+
Play Now (clear queue)
78+
<ha-svg-icon slot="graphic" .path=${mdiPlayBoxMultiple}></ha-svg-icon>
79+
</ha-list-item>
80+
<ha-list-item graphic="icon">
81+
Start Radio
82+
<ha-svg-icon slot="graphic" .path=${mdiAccessPoint}></ha-svg-icon>
83+
</ha-list-item>
84+
<ha-list-item graphic="icon">
85+
Play Now
86+
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
87+
</ha-list-item>
88+
<ha-list-item graphic="icon">
89+
Play Next
90+
<ha-svg-icon slot="graphic" .path=${mdiSkipNext}></ha-svg-icon>
91+
</ha-list-item>
92+
<ha-list-item graphic="icon">
93+
Add to Queue
94+
<ha-svg-icon slot="graphic" .path=${mdiPlaylistPlus}></ha-svg-icon>
95+
</ha-list-item>
96+
<ha-list-item graphic="icon">
97+
Play Next (clear queue)
98+
<ha-svg-icon slot="graphic" .path=${mdiSkipNextCircle}></ha-svg-icon>
99+
</ha-list-item>
100+
`;
101+
}
102+
103+
private getActionIcon(index: number): string {
104+
return [mdiPlayBoxMultiple, mdiAccessPoint, mdiPlay, mdiSkipNext, mdiPlaylistPlus, mdiSkipNextCircle][index];
105+
}
106+
107+
private getActionLabel(index: number): string {
108+
return [
109+
'Play Now (clear queue)',
110+
'Start Radio',
111+
'Play Now',
112+
'Play Next',
113+
'Add to Queue',
114+
'Play Next (clear queue)',
115+
][index];
116+
}
117+
118+
private handleAction(e: CustomEvent) {
119+
const action = PLAY_MENU_ACTIONS[e.detail.index];
120+
if (action) {
121+
this.dispatchEvent(customEvent('play-menu-action', action));
122+
}
123+
}
124+
125+
private selectAction(index: number) {
126+
const action = PLAY_MENU_ACTIONS[index];
127+
if (action) {
128+
this.dispatchEvent(customEvent('play-menu-action', action));
129+
}
130+
}
131+
132+
private closeMenu() {
133+
this.dispatchEvent(customEvent('play-menu-close'));
134+
}
135+
136+
static styles = css`
137+
:host {
138+
display: contents;
139+
}
140+
.inline-menu {
141+
position: relative;
142+
background: var(--card-background-color, var(--primary-background-color));
143+
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
144+
border-radius: 8px;
145+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
146+
min-width: 200px;
147+
padding: 17px 17px;
148+
z-index: 10;
149+
}
150+
.close-btn {
151+
position: absolute;
152+
top: 2px;
153+
right: 2px;
154+
--mdc-icon-button-size: 28px;
155+
--mdc-icon-size: 18px;
156+
color: var(--secondary-text-color);
157+
}
158+
.inline-menu-item {
159+
display: flex;
160+
align-items: center;
161+
gap: 12px;
162+
padding: 10px 16px;
163+
cursor: pointer;
164+
color: var(--primary-text-color);
165+
font-size: 0.9rem;
166+
}
167+
.inline-menu-item:hover {
168+
background: var(--secondary-background-color);
169+
}
170+
.inline-menu-item ha-svg-icon {
171+
--mdc-icon-size: 20px;
172+
flex-shrink: 0;
173+
}
174+
`;
175+
}
176+
177+
customElements.define('sonos-play-menu', PlayMenu);

src/components/selection-actions.ts

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { css, html, LitElement, nothing } from 'lit';
22
import { property } from 'lit/decorators.js';
3-
import { mdiAnimationPlay, mdiPlaylistPlus, mdiSelectInverse, mdiSkipNext } from '@mdi/js';
3+
import { mdiSelectInverse } from '@mdi/js';
44
import { customEvent } from '../utils/utils';
5+
import './play-menu';
6+
import type { PlayMenuAction } from './play-menu';
57

68
export class SelectionActions extends LitElement {
79
@property({ type: Boolean }) hasSelection = false;
@@ -17,45 +19,41 @@ export class SelectionActions extends LitElement {
1719
title="Invert selection"
1820
></ha-icon-button>`
1921
: nothing}
20-
${this.hasSelection
21-
? html`
22-
<ha-icon-button
23-
.path=${mdiAnimationPlay}
24-
@click=${this.playSelected}
25-
title="Play selected"
26-
?disabled=${this.disabled}
27-
></ha-icon-button>
28-
<ha-icon-button
29-
.path=${mdiSkipNext}
30-
@click=${this.queueSelected}
31-
title="Queue selected after current"
32-
?disabled=${this.disabled}
33-
></ha-icon-button>
34-
<ha-icon-button
35-
.path=${mdiPlaylistPlus}
36-
@click=${this.queueSelectedAtEnd}
37-
title="Add selected to end of queue"
38-
?disabled=${this.disabled}
39-
></ha-icon-button>
40-
`
41-
: nothing}
22+
<sonos-play-menu
23+
.hasSelection=${this.hasSelection}
24+
.disabled=${this.disabled}
25+
@play-menu-action=${this.onPlayMenuAction}
26+
></sonos-play-menu>
4227
`;
4328
}
4429

4530
private invertSelection() {
4631
this.dispatchEvent(customEvent('invert-selection'));
4732
}
4833

49-
private playSelected() {
50-
this.dispatchEvent(customEvent('play-selected'));
51-
}
52-
53-
private queueSelected() {
54-
this.dispatchEvent(customEvent('queue-selected'));
55-
}
56-
57-
private queueSelectedAtEnd() {
58-
this.dispatchEvent(customEvent('queue-selected-at-end'));
34+
private onPlayMenuAction(e: CustomEvent<PlayMenuAction>) {
35+
const action = e.detail;
36+
switch (action.enqueue) {
37+
case 'replace':
38+
this.dispatchEvent(customEvent('play-selected', { enqueue: 'replace' }));
39+
break;
40+
case 'play':
41+
if (action.radioMode) {
42+
this.dispatchEvent(customEvent('play-selected', { enqueue: 'play', radioMode: true }));
43+
} else {
44+
this.dispatchEvent(customEvent('play-selected', { enqueue: 'play' }));
45+
}
46+
break;
47+
case 'next':
48+
this.dispatchEvent(customEvent('queue-selected', { enqueue: 'next' }));
49+
break;
50+
case 'add':
51+
this.dispatchEvent(customEvent('queue-selected-at-end', { enqueue: 'add' }));
52+
break;
53+
case 'replace_next':
54+
this.dispatchEvent(customEvent('queue-selected', { enqueue: 'replace_next' }));
55+
break;
56+
}
5957
}
6058

6159
static styles = css`

0 commit comments

Comments
 (0)