Skip to content

Commit d2e84a4

Browse files
committed
Show playing status on tracks and album cover
1 parent 201294c commit d2e84a4

File tree

4 files changed

+188
-15
lines changed

4 files changed

+188
-15
lines changed

packages/components/albums/src/album-detail-page.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { EffectFn } from "@echo/components-shared-controllers/src/effect-fn.controller";
2-
import { Genre, Library, type Album, type AlbumId } from "@echo/core-types";
2+
import {
3+
Genre,
4+
Library,
5+
Player,
6+
type Album,
7+
type AlbumId,
8+
} from "@echo/core-types";
39
import { Option } from "effect";
410
import { LitElement, html, css, nothing } from "lit";
5-
import { customElement, property } from "lit/decorators.js";
11+
import { customElement, property, state } from "lit/decorators.js";
612
import {
713
Path,
814
type RouterLocation,
@@ -11,6 +17,8 @@ import {
1117
import { map } from "lit/directives/map.js";
1218
import "@echo/components-ui-atoms";
1319
import "./playable-album-cover";
20+
import { StreamConsumer } from "@echo/components-shared-controllers";
21+
import { classMap } from "lit/directives/class-map.js";
1422

1523
/**
1624
* Component that displays the details of an album.
@@ -20,6 +28,9 @@ export class AlbumDetail extends LitElement {
2028
@property({ type: Object })
2129
album!: Album;
2230

31+
@state()
32+
playingTrackIndex: number | null = null;
33+
2334
static styles = css`
2435
ol.track-list {
2536
display: flex;
@@ -98,13 +109,35 @@ export class AlbumDetail extends LitElement {
98109
justify-content: space-between;
99110
}
100111
112+
div.track > div {
113+
display: inline-flex;
114+
gap: 0.5rem;
115+
}
116+
117+
div.track-playing {
118+
background-color: var(--background-color-muted);
119+
}
120+
101121
div.track > .duration {
102122
color: var(--secondary-text-color);
103123
}
104124
`;
105125

106-
constructor() {
107-
super();
126+
connectedCallback(): void {
127+
super.connectedCallback();
128+
129+
new StreamConsumer(this, Player.observe, {
130+
item: (playerStatus) => {
131+
if (
132+
playerStatus.status._tag === "Playing" &&
133+
playerStatus.status.album.id === this.album.id
134+
) {
135+
this.playingTrackIndex = playerStatus.status.trackIndex;
136+
} else {
137+
this.playingTrackIndex = null;
138+
}
139+
},
140+
});
108141
}
109142

110143
render() {
@@ -115,6 +148,7 @@ export class AlbumDetail extends LitElement {
115148
<playable-album-cover
116149
.album=${this.album}
117150
detailsAlwaysVisible
151+
?playing=${this.playingTrackIndex !== null}
118152
></playable-album-cover>
119153
<h1>${this.album.name}</h1>
120154
<h5>
@@ -134,13 +168,27 @@ export class AlbumDetail extends LitElement {
134168
<ol class="track-list">
135169
${map(
136170
this.album.tracks,
137-
(track) =>
138-
html`<div class="track">
139-
<li>${track.name}</li>
140-
<span class="duration"
141-
>${this._formatDuration(track.durationInSeconds)}</span
171+
(track, index) =>
172+
html`<li>
173+
<div
174+
class=${classMap({
175+
track: true,
176+
"track-playing": this.playingTrackIndex === index,
177+
})}
142178
>
143-
</div>`,
179+
<div>
180+
${this.playingTrackIndex === index
181+
? html`<animated-volume-icon
182+
size="16"
183+
></animated-volume-icon>`
184+
: nothing}
185+
<span>${track.name}</span>
186+
</div>
187+
<span class="duration"
188+
>${this._formatDuration(track.durationInSeconds)}</span
189+
>
190+
</div>
191+
</li>`,
144192
)}
145193
</ol>
146194
</div>

packages/components/albums/src/playable-album-cover.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { Option } from "effect";
22
import { EffectFn } from "@echo/components-shared-controllers/src/effect-fn.controller";
33
import { Player, type Album } from "@echo/core-types";
44
import { LitElement, html, css } from "lit";
5-
import { customElement, property } from "lit/decorators.js";
5+
import { customElement, property, state } from "lit/decorators.js";
66
import "@echo/components-icons";
77
import "@echo/components-ui-atoms";
8+
import { StreamConsumer } from "@echo/components-shared-controllers";
9+
10+
enum PlayStatus {
11+
Playing,
12+
Paused,
13+
NotPlaying,
14+
}
815

916
/**
1017
* An element that displays the cover of an album from the user's library
@@ -18,7 +25,11 @@ export class PlayableAlbumCover extends LitElement {
1825
@property({ type: Boolean })
1926
detailsAlwaysVisible = false;
2027

28+
@state()
29+
private _playStatus = PlayStatus.NotPlaying;
30+
2131
private _playAlbum = new EffectFn(this, Player.playAlbum);
32+
private _togglePlayback = new EffectFn(this, () => Player.togglePlayback);
2233

2334
static styles = css`
2435
div.album-container {
@@ -122,8 +133,25 @@ export class PlayableAlbumCover extends LitElement {
122133
}
123134
`;
124135

125-
constructor() {
126-
super();
136+
connectedCallback(): void {
137+
super.connectedCallback();
138+
139+
new StreamConsumer(this, Player.observe, {
140+
item: (playerStatus) => {
141+
if (
142+
playerStatus.status._tag === "Stopped" ||
143+
playerStatus.status.album.id !== this.album.id
144+
) {
145+
this._playStatus = PlayStatus.NotPlaying;
146+
return;
147+
}
148+
149+
this._playStatus =
150+
playerStatus.status._tag === "Playing"
151+
? PlayStatus.Playing
152+
: PlayStatus.Paused;
153+
},
154+
});
127155
}
128156

129157
render() {
@@ -142,14 +170,20 @@ export class PlayableAlbumCover extends LitElement {
142170
/>
143171
`}
144172
<button class="play" @click=${this._onPlayClick} title="Play">
145-
<play-icon size="24"></play-icon>
173+
${this._playStatus === PlayStatus.Playing
174+
? html`<pause-icon size="24"></pause-icon>`
175+
: html`<play-icon size="24"></play-icon>`}
146176
</button>
147177
</div>
148178
`;
149179
}
150180

151181
private _onPlayClick() {
152-
this._playAlbum.run(this.album);
182+
if (this._playStatus === PlayStatus.NotPlaying) {
183+
return this._playAlbum.run(this.album);
184+
}
185+
186+
this._togglePlayback.run({});
153187
}
154188
}
155189

packages/components/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./src/prev-icon";
88
export * from "./src/pause-icon";
99
export * from "./src/provider-icon";
1010
export * from "./src/sync-icon";
11+
export * from "./src/volume-icon";
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { LitElement, html } from "lit";
2+
import { customElement, property, state } from "lit/decorators.js";
3+
4+
export enum VolumeIconVariant {
5+
Low = 0,
6+
Medium = 1,
7+
High = 2,
8+
}
9+
10+
/**
11+
* Icon that represents volume, with three variants to represent the amount.
12+
*/
13+
@customElement("volume-icon")
14+
export class VolumeIcon extends LitElement {
15+
@property({ type: Number }) size = 24;
16+
@property({ type: Object }) variant = VolumeIconVariant.Low;
17+
18+
render() {
19+
return this.variant === VolumeIconVariant.Low
20+
? html`<svg
21+
fill="none"
22+
xmlns="http://www.w3.org/2000/svg"
23+
viewBox="0 0 14 20"
24+
height="${this.size}"
25+
width="${this.size}"
26+
>
27+
<path
28+
d="M15 2h-2v2h-2v2H9v2H5v8h4v2h2v2h2v2h2V2zm-4 16v-2H9v-2H7v-4h2V8h2V6h2v12h-2zm6-8h2v4h-2v-4z"
29+
fill="currentColor"
30+
/>
31+
</svg>`
32+
: this.variant === VolumeIconVariant.Medium
33+
? html`<svg
34+
fill="none"
35+
xmlns="http://www.w3.org/2000/svg"
36+
viewBox="0 0 20 20"
37+
height="${this.size}"
38+
width="${this.size}"
39+
>
40+
<path
41+
d="M11 2h2v20h-2v-2H9v-2h2V6H9V4h2V2zM7 8V6h2v2H7zm0 8H3V8h4v2H5v4h2v2zm0 0v2h2v-2H7zm10-6h-2v4h2v-4zm2-2h2v8h-2V8zm0 8v2h-4v-2h4zm0-10v2h-4V6h4z"
42+
fill="currentColor"
43+
/>
44+
</svg>`
45+
: html`<svg
46+
fill="none"
47+
xmlns="http://www.w3.org/2000/svg"
48+
viewBox="0 0 24 20"
49+
height="${this.size}"
50+
width="${this.size}"
51+
>
52+
<path
53+
d="M11 2H9v2H7v2H5v2H1v8h4v2h2v2h2v2h2V2zM7 18v-2H5v-2H3v-4h2V8h2V6h2v12H7zm6-8h2v4h-2v-4zm8-6h-2V2h-6v2h6v2h2v12h-2v2h-6v2h6v-2h2v-2h2V6h-2V4zm-2 4h-2V6h-4v2h4v8h-4v2h4v-2h2V8z"
54+
fill="currentColor"
55+
/>
56+
</svg>`;
57+
}
58+
}
59+
60+
/**
61+
* Animated icon that cycles through the three volume variants each second.
62+
*/
63+
@customElement("animated-volume-icon")
64+
export class AnimatedVolumeIcon extends LitElement {
65+
@property({ type: Number }) size = 24;
66+
67+
@state()
68+
private _variant = VolumeIconVariant.Low;
69+
70+
connectedCallback() {
71+
super.connectedCallback();
72+
setInterval(() => {
73+
this._variant = (this._variant + 1) % 3;
74+
}, 1000);
75+
}
76+
77+
render() {
78+
return html`<volume-icon
79+
size=${this.size}
80+
variant=${this._variant}
81+
></volume-icon>`;
82+
}
83+
}
84+
85+
declare global {
86+
interface HTMLElementTagNameMap {
87+
"animated-volume-icon": AnimatedVolumeIcon;
88+
"volume-icon": VolumeIcon;
89+
}
90+
}

0 commit comments

Comments
 (0)