Skip to content

Commit

Permalink
Show loading states in player and cover
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepyfran committed Dec 24, 2024
1 parent d2e84a4 commit 611ee2e
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 8 deletions.
20 changes: 15 additions & 5 deletions packages/components/albums/src/playable-album-cover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "@echo/components-ui-atoms";
import { StreamConsumer } from "@echo/components-shared-controllers";

enum PlayStatus {
Loading,
Playing,
Paused,
NotPlaying,
Expand Down Expand Up @@ -149,7 +150,9 @@ export class PlayableAlbumCover extends LitElement {
this._playStatus =
playerStatus.status._tag === "Playing"
? PlayStatus.Playing
: PlayStatus.Paused;
: playerStatus.status._tag === "Loading"
? PlayStatus.Loading
: PlayStatus.Paused;
},
});
}
Expand All @@ -169,10 +172,17 @@ export class PlayableAlbumCover extends LitElement {
class="album-cover"
/>
`}
<button class="play" @click=${this._onPlayClick} title="Play">
${this._playStatus === PlayStatus.Playing
? html`<pause-icon size="24"></pause-icon>`
: html`<play-icon size="24"></play-icon>`}
<button
class="play"
@click=${this._onPlayClick}
?disabled=${this._playStatus === PlayStatus.Loading}
title="Play"
>
${this._playStatus === PlayStatus.Loading
? html`<loader-icon size="24"></loader-icon>`
: this._playStatus === PlayStatus.Playing
? html`<pause-icon size="24"></pause-icon>`
: html`<play-icon size="24"></play-icon>`}
</button>
</div>
`;
Expand Down
1 change: 1 addition & 0 deletions packages/components/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./src/chevron-icon";
export * from "./src/cross-icon";
export * from "./src/done-icon";
export * from "./src/loader-icon";
export * from "./src/next-icon";
export * from "./src/play-icon";
export * from "./src/plus-icon";
Expand Down
45 changes: 45 additions & 0 deletions packages/components/icons/src/loader-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";

/**
* Icon that shows a loader.
*/
@customElement("loader-icon")
export class LoaderIcon extends LitElement {
@property({ type: Number }) size = 24;
@property({ type: Boolean }) animated = true;

static styles = css`
.animated {
animation: spin 2s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`;

render() {
return html`<svg
class=${this.animated ? "animated" : ""}
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox=${`0 0 ${this.size} ${this.size}`}
height=${this.size}
width=${this.size}
>
<path
d="M13 2h-2v6h2V2zm0 14h-2v6h2v-6zm9-5v2h-6v-2h6zM8 13v-2H2v2h6zm7-6h2v2h-2V7zm4-2h-2v2h2V5zM9 7H7v2h2V7zM5 5h2v2H5V5zm10 12h2v2h2v-2h-2v-2h-2v2zm-8 0v-2h2v2H7v2H5v-2h2z"
fill="currentColor"
/>
</svg>`;
}
}

declare global {
interface HTMLElementTagNameMap {
"loader-icon": LoaderIcon;
}
}
25 changes: 24 additions & 1 deletion packages/components/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Match, Option } from "effect";
import { EffectFn } from "@echo/components-shared-controllers/src/effect-fn.controller";
import { ButtonType } from "@echo/components-ui-atoms";
import "@echo/components-icons";
import { classMap } from "lit/directives/class-map.js";

/**
* Component that displays the current status of the player.
Expand All @@ -28,6 +29,7 @@ export class EchoPlayer extends LitElement {
it, like WebScrobbler to scrobble tracks.
*/
Match.value(playerState.status).pipe(
Match.tag("Loading", () => {}),
Match.tag("Playing", ({ album, trackIndex }) => {
const track = album.tracks[trackIndex];
this.dispatchEvent(
Expand Down Expand Up @@ -74,6 +76,22 @@ export class EchoPlayer extends LitElement {
border-radius: 5%;
}
.pulsating {
animation: pulsate 1s ease-out infinite;
}
@keyframes pulsate {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.track-info {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -105,7 +123,12 @@ export class EchoPlayer extends LitElement {
Match.tag("Paused", (st) => this._renderActivePlayer(player, st)),
Match.orElse(
() => html`
<div class="current-track">
<div
class=${classMap({
"current-track": true,
pulsating: player.status._tag === "Loading",
})}
>
<h5 class="logo">echo</h5>
</div>
`,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/types/src/model/player-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import type { Album } from "./album";
* Defines whether the player is playing, paused or stopped.
*/
export type PlayingStatus =
| { _tag: "Loading"; album: Album; trackIndex: number }
| { _tag: "Playing"; album: Album; trackIndex: number }
| { _tag: "Paused"; album: Album; trackIndex: number }
| { _tag: "Stopped" };

export const { Playing, Paused, Stopped } = Data.taggedEnum<PlayingStatus>();
export const { Loading, Playing, Paused, Stopped } =
Data.taggedEnum<PlayingStatus>();

/**
* Represents the current state of the player.
Expand Down
2 changes: 1 addition & 1 deletion packages/services/app-init/src/app-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ const syncPageTitleWithPlayer = (player: IPlayer) =>
yield* playerState.changes.pipe(
Stream.runForEach((state) =>
Match.value(state.status).pipe(
Match.tag("Stopped", () =>
Match.tag("Stopped", "Loading", () =>
Effect.sync(() => (document.title = "Echo")),
),
Match.tag("Playing", "Paused", ({ album, trackIndex }) =>
Expand Down
17 changes: 17 additions & 0 deletions packages/services/player/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type PlayerState,
type Track,
type Album,
Loading,
} from "@echo/core-types";
import {
Array,
Expand Down Expand Up @@ -243,6 +244,7 @@ const consumeCommandsInBackground = (
UpdateState({
updateFn: (state) =>
Match.value(state.status).pipe(
Match.tag("Loading", () => state),
Match.tag("Playing", ({ album, trackIndex }) =>
isPlaying
? state
Expand Down Expand Up @@ -313,6 +315,11 @@ const playTracks = ({
);

yield* commandQueue.offer(SyncPlayerState({ withMediaPlayer: player }));
yield* commandQueue.offer(
UpdateState({
updateFn: toLoadingState(album, trackIndex),
}),
);
yield* playTrack(provider, player, requestedTrack.value);
yield* commandQueue.offer(
UpdateState({
Expand Down Expand Up @@ -467,6 +474,7 @@ const toPlayingState =
? [
...currentState.previouslyPlayedAlbums,
...Match.value(currentState.status).pipe(
Match.tag("Loading", () => [album]),
Match.tag("Playing", ({ album }) => [album]),
Match.tag("Paused", ({ album }) => [album]),
Match.tag("Stopped", () => []),
Expand All @@ -478,6 +486,15 @@ const toPlayingState =
} satisfies PlayerState;
};

/**
* Sets the player state to loading the given track from the given album.
*/
const toLoadingState =
(album: Album, trackIndex: number) => (currentState: PlayerState) => ({
...currentState,
status: Loading({ album, trackIndex }),
});

const PlayerLiveWithState = Layer.scoped(Player, makePlayer);

const PlayerStateLive = Layer.effect(
Expand Down

0 comments on commit 611ee2e

Please sign in to comment.