Skip to content

Commit 307c9bb

Browse files
authored
Merge pull request #26 from SevenOutman/beta
Graduate 1.6 Beta
2 parents 0ffa21f + bed87db commit 307c9bb

File tree

7 files changed

+156
-18
lines changed

7 files changed

+156
-18
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
push:
5-
branches: [master, alpha]
5+
branches: [master, beta]
66
pull_request:
77
branches: [master]
88

demo/index.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ function App() {
6262
}}
6363
>
6464
<div style={{ width: 600 }}>
65-
<APlayer audio={playlist} theme="auto" initialLoop="all" />
65+
<APlayer
66+
audio={playlist}
67+
appearance="fixed"
68+
theme="auto"
69+
initialLoop="all"
70+
/>
6671
</div>
6772
</div>
6873
);

src/components/controller.tsx

+39-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import { useCallback } from "react";
2+
import { clsx } from "clsx";
13
import { ReactComponent as IconMenu } from "../assets/menu.svg";
4+
import { ReactComponent as IconPlay } from "../assets/play.svg";
5+
import { ReactComponent as IconPause } from "../assets/pause.svg";
6+
import { ReactComponent as IconSkip } from "../assets/skip.svg";
7+
import { ReactComponent as IconLrc } from "../assets/lrc.svg";
28
import { ReactComponent as IconOrderList } from "../assets/order-list.svg";
39
import { ReactComponent as IconOrderRandom } from "../assets/order-random.svg";
410
import { ReactComponent as IconLoopAll } from "../assets/loop-all.svg";
511
import { ReactComponent as IconLoopOne } from "../assets/loop-one.svg";
612
import { ReactComponent as IconLoopNone } from "../assets/loop-none.svg";
713
import { formatAudioDuration } from "../utils/formatAudioDuration";
814
import { ProgressBar } from "./progress";
9-
import React, { useCallback } from "react";
1015
import { PlaylistLoop, PlaylistOrder } from "../hooks/usePlaylist";
1116
import { Volume } from "./volume";
1217

@@ -25,6 +30,12 @@ type PlaybackControlsProps = {
2530
loop: PlaylistLoop;
2631
onLoopChange: (loop: PlaylistLoop) => void;
2732
onSeek?: (second: number) => void;
33+
isPlaying: boolean;
34+
onTogglePlay?: () => void;
35+
onSkipForward?: () => void;
36+
onSkipBack?: () => void;
37+
showLyrics?: boolean;
38+
onToggleLyrics?: () => void;
2839
};
2940

3041
export function PlaybackControls({
@@ -42,6 +53,12 @@ export function PlaybackControls({
4253
loop,
4354
onLoopChange,
4455
onSeek,
56+
isPlaying,
57+
onTogglePlay,
58+
onSkipForward,
59+
onSkipBack,
60+
showLyrics = true,
61+
onToggleLyrics,
4562
}: PlaybackControlsProps) {
4663
// Switch order between "list" and "random"
4764
const handleOrderButtonClick = useCallback(() => {
@@ -96,9 +113,18 @@ export function PlaybackControls({
96113
{formatAudioDuration(audioDurationSeconds)}
97114
</span>
98115
</span>
99-
<span className="aplayer-icon aplayer-icon-back"></span>
100-
<span className="aplayer-icon aplayer-icon-play"></span>
101-
<span className="aplayer-icon aplayer-icon-forward"></span>
116+
<span className="aplayer-icon aplayer-icon-back" onClick={onSkipBack}>
117+
<IconSkip />
118+
</span>
119+
<span className="aplayer-icon aplayer-icon-play" onClick={onTogglePlay}>
120+
{isPlaying ? <IconPause /> : <IconPlay />}
121+
</span>
122+
<span
123+
className="aplayer-icon aplayer-icon-forward"
124+
onClick={onSkipForward}
125+
>
126+
<IconSkip />
127+
</span>
102128
<Volume
103129
themeColor={themeColor}
104130
volume={volume}
@@ -130,7 +156,15 @@ export function PlaybackControls({
130156
>
131157
<IconMenu />
132158
</button>
133-
<button className="aplayer-icon aplayer-icon-lrc"></button>
159+
<button
160+
type="button"
161+
className={clsx("aplayer-icon aplayer-icon-lrc", {
162+
"aplayer-icon-lrc-inactivity": !showLyrics,
163+
})}
164+
onClick={onToggleLyrics}
165+
>
166+
<IconLrc />
167+
</button>
134168
</div>
135169
</div>
136170
);

src/components/list.tsx

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { useCallback, useEffect, useRef } from "react";
12
import { clsx } from "clsx";
23
import { defaultThemeColor } from "../constants";
34
import type { ArtistInfo, AudioInfo } from "../types";
4-
import { useCallback } from "react";
55

66
type PlaylistProps = {
77
open: boolean;
@@ -29,8 +29,26 @@ export function Playlist({
2929
return artist.name ?? "Audio artist";
3030
}, []);
3131

32+
const listRef = useRef<HTMLDivElement>(null);
33+
34+
useEffect(() => {
35+
if (listRef.current) {
36+
const listElement = listRef.current;
37+
38+
listElement.style.maxHeight = `${Math.min(
39+
listElement.scrollHeight,
40+
listMaxHeight ?? Infinity
41+
)}px`;
42+
43+
return () => {
44+
listElement.removeAttribute("style");
45+
};
46+
}
47+
}, [listMaxHeight]);
48+
3249
return (
3350
<div
51+
ref={listRef}
3452
className={clsx("aplayer-list", {
3553
"aplayer-list-hide": !open,
3654
})}

src/components/lyrics.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import React, { useMemo } from "react";
22
import { clsx } from "clsx";
33

44
type LyricsProps = {
5+
show: boolean;
56
lrcText?: string;
67
currentTime: number;
78
};
89

9-
export function Lyrics({ lrcText, currentTime }: LyricsProps) {
10+
export function Lyrics({ show, lrcText, currentTime }: LyricsProps) {
1011
const lines = useMemo(() => parseLrc(lrcText), [lrcText]);
1112

1213
const currentLineIndex = useMemo(() => {
@@ -29,7 +30,11 @@ export function Lyrics({ lrcText, currentTime }: LyricsProps) {
2930
}, [currentLineIndex]);
3031

3132
return (
32-
<div className="aplayer-lrc">
33+
<div
34+
className={clsx("aplayer-lrc", {
35+
"aplayer-lrc-hide": !show,
36+
})}
37+
>
3338
{lrcText ? (
3439
<div className="aplayer-lrc-contents" style={transformStyle}>
3540
{lines.map(([, text], index) => (

src/components/player.tsx

+70-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { clsx } from "clsx";
33

44
import { ReactComponent as IconPlay } from "../assets/play.svg";
55
import { ReactComponent as IconPause } from "../assets/pause.svg";
6+
import { ReactComponent as IconRight } from "../assets/right.svg";
67
import type { ArtistInfo, AudioInfo } from "../types";
78
import { Playlist } from "./list";
89
import { PlaybackControls } from "./controller";
@@ -31,6 +32,11 @@ type APlayerProps = {
3132
*/
3233
volume?: number;
3334

35+
/**
36+
* @default "normal"
37+
*/
38+
appearance?: "normal" | "fixed";
39+
3440
/**
3541
* @default "all"
3642
*/
@@ -52,6 +58,7 @@ type APlayerProps = {
5258
export function APlayer({
5359
theme = defaultThemeColor,
5460
audio,
61+
appearance = "normal",
5562
volume = 0.7,
5663
initialLoop,
5764
initialOrder,
@@ -160,17 +167,42 @@ export function APlayer({
160167
);
161168
}, []);
162169

170+
const [mini, setMini] = useState(false);
171+
172+
const [displayLyrics, setDisplayLyrics] = useState(true);
173+
174+
const bodyRef = useRef<HTMLDivElement>(null);
175+
176+
useEffect(() => {
177+
if (appearance === "fixed") {
178+
if (bodyRef.current) {
179+
const bodyElement = bodyRef.current;
180+
// Explicitly set width on the body element
181+
// to ensure the width transition works
182+
bodyElement.style.width = bodyElement.offsetWidth - 18 + "px";
183+
184+
return () => {
185+
bodyElement.removeAttribute("style");
186+
};
187+
}
188+
}
189+
}, [appearance]);
190+
163191
return (
164192
<div
165193
className={clsx("aplayer", {
194+
"aplayer-fixed": appearance === "fixed",
166195
"aplayer-loading": audioControl.isLoading,
167196
"aplayer-withlist": hasPlaylist,
168-
"aplayer-withlrc": Boolean(playlist.currentSong.lrc),
197+
"aplayer-withlrc":
198+
Boolean(playlist.currentSong.lrc) && appearance !== "fixed",
199+
"aplayer-narrow": mini,
169200
})}
170201
>
171-
<div className="aplayer-body">
202+
<div ref={bodyRef} className="aplayer-body">
172203
<div
173204
className="aplayer-pic"
205+
onClick={handlePlayButtonClick}
174206
style={{
175207
backgroundImage: `url("${playlist.currentSong?.cover}")`,
176208
}}
@@ -180,7 +212,6 @@ export function APlayer({
180212
"aplayer-button",
181213
audioControl.isPlaying ? "aplayer-pause" : "aplayer-play"
182214
)}
183-
onClick={handlePlayButtonClick}
184215
>
185216
{audioControl.isPlaying ? <IconPause /> : <IconPlay />}
186217
</div>
@@ -195,10 +226,13 @@ export function APlayer({
195226
- {renderArtist(playlist.currentSong?.artist)}
196227
</span>
197228
</div>
198-
<Lyrics
199-
lrcText={playlist.currentSong.lrc}
200-
currentTime={audioControl.currentTime ?? 0}
201-
/>
229+
{appearance === "fixed" ? null : (
230+
<Lyrics
231+
show={displayLyrics}
232+
lrcText={playlist.currentSong.lrc}
233+
currentTime={audioControl.currentTime ?? 0}
234+
/>
235+
)}
202236
<PlaybackControls
203237
volume={audioControl.volume ?? volume}
204238
onChangeVolume={audioControl.setVolume}
@@ -214,12 +248,33 @@ export function APlayer({
214248
onOrderChange={playlist.setOrder}
215249
loop={playlist.loop}
216250
onLoopChange={playlist.setLoop}
251+
isPlaying={audioControl.isPlaying ?? false}
252+
onTogglePlay={handlePlayButtonClick}
253+
onSkipForward={() => {
254+
if (playlist.hasNextSong) {
255+
playlist.next();
256+
}
257+
}}
258+
onSkipBack={() => {
259+
playlist.previous();
260+
}}
261+
showLyrics={displayLyrics}
262+
onToggleLyrics={() => {
263+
setDisplayLyrics((prev) => !prev);
264+
}}
217265
/>
218266
</div>
219267
<div className="aplayer-notice" style={notice.style}>
220268
{notice.text}
221269
</div>
222-
<div className="aplayer-miniswitcher"></div>
270+
<div
271+
className="aplayer-miniswitcher"
272+
onClick={() => setMini((prev) => !prev)}
273+
>
274+
<button className="aplayer-icon">
275+
<IconRight />
276+
</button>
277+
</div>
223278
</div>
224279
{hasPlaylist ? (
225280
<Playlist
@@ -231,6 +286,13 @@ export function APlayer({
231286
listMaxHeight={listMaxHeight}
232287
/>
233288
) : null}
289+
{appearance === "fixed" && (
290+
<Lyrics
291+
show={displayLyrics}
292+
lrcText={playlist.currentSong.lrc}
293+
currentTime={audioControl.currentTime ?? 0}
294+
/>
295+
)}
234296
</div>
235297
);
236298
}

src/hooks/usePlaylist.ts

+14
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type PlaylistState<T> = Readonly<{
1414
currentSong: T;
1515
hasNextSong: boolean;
1616
next: () => void;
17+
previous: () => void;
1718
prioritize: (song: T) => void;
1819
order: PlaylistOrder;
1920
setOrder: (order: PlaylistOrder) => void;
@@ -79,6 +80,18 @@ export function usePlaylist<T, K>(
7980
}
8081
}, [nextSong]);
8182

83+
const previous = useCallback(() => {
84+
setCurrentSong((prev) => {
85+
const currentSongIndex = list.indexOf(prev);
86+
87+
if (currentSongIndex > 0) {
88+
return list[currentSongIndex - 1];
89+
}
90+
91+
return prev;
92+
});
93+
}, [list]);
94+
8295
const prioritize = useCallback((song: T) => {
8396
setCurrentSong(song);
8497
}, []);
@@ -87,6 +100,7 @@ export function usePlaylist<T, K>(
87100
currentSong,
88101
hasNextSong: typeof nextSong !== "undefined",
89102
next,
103+
previous,
90104
prioritize,
91105
order,
92106
setOrder,

0 commit comments

Comments
 (0)