Skip to content

Commit

Permalink
Show playing status on tracks and album cover
Browse files Browse the repository at this point in the history
sleepyfran committed Dec 24, 2024

Verified

This commit was signed with the committer’s verified signature.
sleepyfran Fran González
1 parent 201294c commit d2e84a4
Showing 4 changed files with 188 additions and 15 deletions.
68 changes: 58 additions & 10 deletions packages/components/albums/src/album-detail-page.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { EffectFn } from "@echo/components-shared-controllers/src/effect-fn.controller";
import { Genre, Library, type Album, type AlbumId } from "@echo/core-types";
import {
Genre,
Library,
Player,
type Album,
type AlbumId,
} from "@echo/core-types";
import { Option } from "effect";
import { LitElement, html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import {
Path,
type RouterLocation,
@@ -11,6 +17,8 @@ import {
import { map } from "lit/directives/map.js";
import "@echo/components-ui-atoms";
import "./playable-album-cover";
import { StreamConsumer } from "@echo/components-shared-controllers";
import { classMap } from "lit/directives/class-map.js";

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

@state()
playingTrackIndex: number | null = null;

static styles = css`
ol.track-list {
display: flex;
@@ -98,13 +109,35 @@ export class AlbumDetail extends LitElement {
justify-content: space-between;
}
div.track > div {
display: inline-flex;
gap: 0.5rem;
}
div.track-playing {
background-color: var(--background-color-muted);
}
div.track > .duration {
color: var(--secondary-text-color);
}
`;

constructor() {
super();
connectedCallback(): void {
super.connectedCallback();

new StreamConsumer(this, Player.observe, {
item: (playerStatus) => {
if (
playerStatus.status._tag === "Playing" &&
playerStatus.status.album.id === this.album.id
) {
this.playingTrackIndex = playerStatus.status.trackIndex;
} else {
this.playingTrackIndex = null;
}
},
});
}

render() {
@@ -115,6 +148,7 @@ export class AlbumDetail extends LitElement {
<playable-album-cover
.album=${this.album}
detailsAlwaysVisible
?playing=${this.playingTrackIndex !== null}
></playable-album-cover>
<h1>${this.album.name}</h1>
<h5>
@@ -134,13 +168,27 @@ export class AlbumDetail extends LitElement {
<ol class="track-list">
${map(
this.album.tracks,
(track) =>
html`<div class="track">
<li>${track.name}</li>
<span class="duration"
>${this._formatDuration(track.durationInSeconds)}</span
(track, index) =>
html`<li>
<div
class=${classMap({
track: true,
"track-playing": this.playingTrackIndex === index,
})}
>
</div>`,
<div>
${this.playingTrackIndex === index
? html`<animated-volume-icon
size="16"
></animated-volume-icon>`
: nothing}
<span>${track.name}</span>
</div>
<span class="duration"
>${this._formatDuration(track.durationInSeconds)}</span
>
</div>
</li>`,
)}
</ol>
</div>
44 changes: 39 additions & 5 deletions packages/components/albums/src/playable-album-cover.ts
Original file line number Diff line number Diff line change
@@ -2,9 +2,16 @@ import { Option } from "effect";
import { EffectFn } from "@echo/components-shared-controllers/src/effect-fn.controller";
import { Player, type Album } from "@echo/core-types";
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import "@echo/components-icons";
import "@echo/components-ui-atoms";
import { StreamConsumer } from "@echo/components-shared-controllers";

enum PlayStatus {
Playing,
Paused,
NotPlaying,
}

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

@state()
private _playStatus = PlayStatus.NotPlaying;

private _playAlbum = new EffectFn(this, Player.playAlbum);
private _togglePlayback = new EffectFn(this, () => Player.togglePlayback);

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

constructor() {
super();
connectedCallback(): void {
super.connectedCallback();

new StreamConsumer(this, Player.observe, {
item: (playerStatus) => {
if (
playerStatus.status._tag === "Stopped" ||
playerStatus.status.album.id !== this.album.id
) {
this._playStatus = PlayStatus.NotPlaying;
return;
}

this._playStatus =
playerStatus.status._tag === "Playing"
? PlayStatus.Playing
: PlayStatus.Paused;
},
});
}

render() {
@@ -142,14 +170,20 @@ export class PlayableAlbumCover extends LitElement {
/>
`}
<button class="play" @click=${this._onPlayClick} title="Play">
<play-icon size="24"></play-icon>
${this._playStatus === PlayStatus.Playing
? html`<pause-icon size="24"></pause-icon>`
: html`<play-icon size="24"></play-icon>`}
</button>
</div>
`;
}

private _onPlayClick() {
this._playAlbum.run(this.album);
if (this._playStatus === PlayStatus.NotPlaying) {
return this._playAlbum.run(this.album);
}

this._togglePlayback.run({});
}
}

1 change: 1 addition & 0 deletions packages/components/icons/index.ts
Original file line number Diff line number Diff line change
@@ -8,3 +8,4 @@ export * from "./src/prev-icon";
export * from "./src/pause-icon";
export * from "./src/provider-icon";
export * from "./src/sync-icon";
export * from "./src/volume-icon";
90 changes: 90 additions & 0 deletions packages/components/icons/src/volume-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";

export enum VolumeIconVariant {
Low = 0,
Medium = 1,
High = 2,
}

/**
* Icon that represents volume, with three variants to represent the amount.
*/
@customElement("volume-icon")
export class VolumeIcon extends LitElement {
@property({ type: Number }) size = 24;
@property({ type: Object }) variant = VolumeIconVariant.Low;

render() {
return this.variant === VolumeIconVariant.Low
? html`<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 20"
height="${this.size}"
width="${this.size}"
>
<path
d="M15 2h-2v2h-2v2H9v2H5v8h4v2h2v2h2v2h2V2zm-4 16v-2H9v-2H7v-4h2V8h2V6h2v12h-2zm6-8h2v4h-2v-4z"
fill="currentColor"
/>
</svg>`
: this.variant === VolumeIconVariant.Medium
? html`<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
height="${this.size}"
width="${this.size}"
>
<path
d="M11 2h2v20h-2v-2H9v-2h2V6H9V4h2V2zM7 8V6h2v2H7zm0 8H3V8h4v2H5v4h2v2zm0 0v2h2v-2H7zm10-6h-2v4h2v-4zm2-2h2v8h-2V8zm0 8v2h-4v-2h4zm0-10v2h-4V6h4z"
fill="currentColor"
/>
</svg>`
: html`<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 20"
height="${this.size}"
width="${this.size}"
>
<path
d="M11 2H9v2H7v2H5v2H1v8h4v2h2v2h2v2h2V2zM7 18v-2H5v-2H3v-4h2V8h2V6h2v12H7zm6-8h2v4h-2v-4zm8-6h-2V2h-6v2h6v2h2v12h-2v2h-6v2h6v-2h2v-2h2V6h-2V4zm-2 4h-2V6h-4v2h4v8h-4v2h4v-2h2V8z"
fill="currentColor"
/>
</svg>`;
}
}

/**
* Animated icon that cycles through the three volume variants each second.
*/
@customElement("animated-volume-icon")
export class AnimatedVolumeIcon extends LitElement {
@property({ type: Number }) size = 24;

@state()
private _variant = VolumeIconVariant.Low;

connectedCallback() {
super.connectedCallback();
setInterval(() => {
this._variant = (this._variant + 1) % 3;
}, 1000);
}

render() {
return html`<volume-icon
size=${this.size}
variant=${this._variant}
></volume-icon>`;
}
}

declare global {
interface HTMLElementTagNameMap {
"animated-volume-icon": AnimatedVolumeIcon;
"volume-icon": VolumeIcon;
}
}

0 comments on commit d2e84a4

Please sign in to comment.