new player
Seto Elkahfi committed
Aug 1, 2024 at 23:03 UTC
14a8023f7d2d17cb5fcae9cf5aa05a4e61cd1b0e
5 files changed
+199
-130
frontend/splitfire-desktop/app/play/_components/audio-info-middle.tsx
new
+55
@@ -0,0 +1,55 @@
1
+import { useLogger } from "@/app/_src/lib/logger";
2
+import { useCountDown } from "@/app/_src/lib/useCountDown";
3
+import { useEffect } from "react";
4
+
5
+export function AudioInfoMiddle({
6
+ counterDownTimer,
7
+}: {
8
+ counterDownTimer: number;
9
+}) {
10
+ const timePlayed = "0:01";
11
+ const timeTotal = "3:20";
12
+ return (
13
+ <div className="space-y-2">
14
+ <div className="flex-auto flex items-center justify-evenly">
15
+ <CountDownView
16
+ seconds={counterDownTimer}
17
+ type="audio"
18
+ onCountDownEnd={() => {}}
19
+ />
20
+ </div>
21
+ <div className="flex justify-between text-sm leading-6 font-medium tabular-nums">
22
+ <div className="text-cyan-500 dark:text-slate-100">{timePlayed}</div>
23
+ <div className="text-slate-500 dark:text-slate-400">{timeTotal}</div>
24
+ </div>
25
+ </div>
26
+ );
27
+}
28
+
29
+function CountDownView(props: {
30
+ seconds: number;
31
+ type: string;
32
+ onCountDownEnd?: () => void;
33
+}) {
34
+ const log = useLogger("CountDownView");
35
+ const { secondsLeft, startTimer } = useCountDown();
36
+
37
+ useEffect(() => {
38
+ log.debug("start counting down seconds:", props.seconds);
39
+ startTimer(props.seconds);
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, []);
42
+
43
+ if (secondsLeft === 0 && props.onCountDownEnd) {
44
+ log.debug("CountDownView: onCountDownEnd");
45
+ props.onCountDownEnd();
46
+ }
47
+
48
+ const displayText = secondsLeft === 0 ? props.type : secondsLeft;
49
+
50
+ return (
51
+ <div className="text-red-500 dark:text-slate-100">
52
+ <h1>{displayText}</h1>
53
+ </div>
54
+ );
55
+}
frontend/splitfire-desktop/app/play/_components/audio-info.tsx
new
+28
@@ -0,0 +1,28 @@
1
+import Image from "next/image";
2
+
3
+export function AudioInfo(props: { }) {
4
+ const numberPlayed = 785;
5
+ const difficulty = "Difficulty: Hard";
6
+ const imageUrl = "https://img.freepik.com/free-psd/square-flyer-template-maximalist-business_23-2148524497.jpg?w=1800&t=st=1699458420~exp=1699459020~hmac=5b00d72d6983d04966cc08ccd0fc1f80ad0d7ba75ec20316660e11efd18133cd"
7
+ return (
8
+ <div className="flex items-center space-x-4">
9
+ <Image
10
+ src={imageUrl}
11
+ alt=""
12
+ width="88"
13
+ height="88"
14
+ className="flex-none rounded-lg bg-slate-100"
15
+ loading="lazy"
16
+ />
17
+ <div className="min-w-0 flex-auto space-y-1 font-semibold">
18
+ <p className="text-cyan-500 dark:text-cyan-400 text-sm leading-6">
19
+ <abbr title="Track">Played:</abbr> {numberPlayed}
20
+ </p>
21
+ <h2 className="text-slate-500 dark:text-slate-400 text-sm leading-6 truncate">
22
+ Music: New Album The Lorem
23
+ </h2>
24
+ <p className="text-slate-900 dark:text-slate-50 text-lg">{difficulty}</p>
25
+ </div>
26
+ </div>
27
+ );
28
+}
frontend/splitfire-desktop/app/play/_components/control-button.tsx
+77
-36
@@ -1,43 +1,84 @@
1
-import { Col, Row } from "react-bootstrap"
2
-import { BsPlayBtnFill, BsStopBtnFill, BsFillRecordBtnFill, BsPauseBtnFill } from "react-icons/bs"
1
+import {
2
+ BookmarkIcon,
3
+ PauseIcon,
4
+ CircleIcon,
5
+ BookmarkFilledIcon,
6
+ ResumeIcon,
7
+ PlayIcon,
8
+ ResetIcon,
9
+} from "@radix-ui/react-icons";
10
+import { useLogger } from "@/app/_src/lib/logger";
11
+import { useState } from "react";
12
4
-export function ControlButtons(props: { isPlaying: boolean, isRecording: boolean, onClick: () => void, onStop: () => void, onRecord: () => void }) {
13
+export function ControlButtons(props: {
14
+ isPlaying: boolean;
15
+ isRecording: boolean;
16
+ onClick: () => void;
17
+ onStop: () => void;
18
+ onRecord: () => void;
19
+ onResume: () => void;
20
+}) {
21
+ const log = useLogger("ControlButtonsV2");
22
+ const { isPlaying, isRecording } = props;
23
+ const [isBookmarked, setIsBookmarked] = useState(false);
24
6
- const { isPlaying, isRecording, onClick, onStop, onRecord } = props
25
+ const toggleBookmark = () => {
26
+ log.debug("toggleBookmark");
27
+ setIsBookmarked(!isBookmarked);
28
+ };
29
8
- const _onPlayOrPause = () => {
9
- if (isRecording) return
30
+ const bookMarkText = isBookmarked
31
+ ? "Remove from bookmarks"
32
+ : "Add to bookmarks";
33
+ const recordingText = isRecording ? "Recording" : "Stop recording";
34
11
- onClick()
35
+ let playPauseResumeIcon = isPlaying ? (
36
+ <PauseIcon color={"black"} width="24" height="24" />
37
+ ) : (
38
+ <PlayIcon color={"black"} width="24" height="24" />
39
+ );
40
+ if (isRecording) {
41
+ playPauseResumeIcon = <ResumeIcon color={"black"} width="24" height="24" />;
42
}
43
14
- return <>
15
- <Col xs={{span: 1, offset: 4}} className="mb-3 mt-3" color="red" >
16
- { isPlaying ? <BsPauseBtnFill onClick={_onPlayOrPause} size={32} style={ { cursor: isRecording ? undefined : "pointer"}} title="Pause" color={ isRecording ? "grey" : "white"} />
17
- : <BsPlayBtnFill onClick={_onPlayOrPause} size={32} style={ { cursor: isRecording ? undefined : "pointer"}} title="Play" color={ isRecording ? "grey" : "white" } />
18
- }
19
- </Col>
20
- <Col xs={{span: 1}} className="mb-3 mt-3">
21
- <BsStopBtnFill
22
- onClick={() => {
23
- if (isRecording) return
24
-
25
- onStop()
26
- }}
27
- size={32}
28
- style={{ cursor: isRecording ? undefined : "pointer"}}
29
- title="Stop"
30
- color={ isRecording ? "grey" : "white" }
31
- />
32
- </Col>
33
- <Col xs={{span: 1}} className="mb-3 mt-3">
34
- <BsFillRecordBtnFill
35
- onClick={onRecord}
36
- size={32}
37
- style={{ cursor: "pointer"}}
38
- title={ isRecording ? "Stop" : "Record"}
39
- color={ isRecording ? "green" : "red" }
40
- />
41
- </Col>
42
- </>
44
+ return (
45
+ <div className="bg-slate-50 text-slate-500 py-6 dark:bg-slate-600 dark:text-slate-200 rounded-b-xl flex items-center">
46
+ <div className="flex-auto flex items-center justify-evenly">
47
+ <button
48
+ type="button"
49
+ aria-label={bookMarkText}
50
+ onClick={toggleBookmark}
51
+ title={bookMarkText}
52
+ >
53
+ {isBookmarked ? (
54
+ <BookmarkFilledIcon color={"black"} width="24" height="24" />
55
+ ) : (
56
+ <BookmarkIcon color={"black"} width="24" height="24" />
57
+ )}
58
+ </button>
59
+ <button
60
+ type="button"
61
+ aria-label="Record"
62
+ disabled={isPlaying}
63
+ title={recordingText}
64
+ >
65
+ <CircleIcon
66
+ color={isRecording ? "blue" : "red"}
67
+ width="30"
68
+ height="32"
69
+ />
70
+ </button>
71
+ <button
72
+ type="button"
73
+ aria-label="Play/Pause/Resume"
74
+ disabled={isRecording}
75
+ >
76
+ {playPauseResumeIcon}
77
+ </button>
78
+ <button type="button" aria-label="Restart" disabled={isRecording}>
79
+ <ResetIcon color={"black"} width="24" height="24" />
80
+ </button>
81
+ </div>
82
+ </div>
83
+ );
84
}
frontend/splitfire-desktop/app/play/_components/player.tsx
+27
-80
@@ -1,12 +1,7 @@
1
"use client";
2
3
-import { ControlButtonsView } from "@/app/_src/components/player/ControlButtons";
4
-import { CountDownView } from "@/app/_src/components/player/CountDownView";
5
-import { HideShowToggleView } from "@/app/_src/components/player/HideShowToggle";
3
import { ModeDemucs } from "@/app/_src/components/player/models/Mode";
4
import { PlayerState, PlayerVolume } from "@/app/_src/components/player/Player";
8
-import { RecordingView } from "@/app/_src/components/player/RecordingView";
9
-import { VolumeSliderView } from "@/app/_src/components/player/VolumeSliderView";
5
import { useLogger } from "@/app/_src/lib/logger";
6
import {
7
TAURI_PLAYER_PAUSED,
@@ -20,9 +15,9 @@ import {
15
} from "@/app/_src/lib/tauriHandler";
16
import { invoke } from "@tauri-apps/api";
17
import { useState } from "react";
23
-import { Row, Col, Container } from "react-bootstrap";
24
-import { VolumeSlider } from "./volume-slider";
18
import { ControlButtons } from "./control-button";
19
+import { AudioInfo } from "./audio-info";
20
+import { AudioInfoMiddle } from "./audio-info-middle";
21
22
export default function Player({
23
audioId,
@@ -50,8 +45,8 @@ export default function Player({
45
const [otherVolume, setOtherVolume] = useState<string>("100");
46
47
// Player functions
53
- const _onVolumeChange = (mode: ModeDemucs, value: string) => {
54
- log.debug("_onVolumeChange", mode, value);
48
+ const onVolumeChange = (mode: ModeDemucs, value: string) => {
49
+ log.debug("onVolumeChange", mode, value);
50
switch (mode) {
51
case ModeDemucs.Vocals:
52
setVocalsVolume(value);
@@ -74,7 +69,7 @@ export default function Player({
69
//_toggleRecording()
70
};
71
77
- const _toggleRecording = async () => {
72
+ const toggleRecording = async () => {
73
try {
74
// If we are playing playbacks, stop it before recording.
75
if (playerState === PlayerState.PLAYING) {
@@ -88,7 +83,7 @@ export default function Player({
83
84
// If we're already recording, we want to stop it and return.
85
if (playerState === PlayerState.RECORDING) {
91
- _stopRecording();
86
+ stopRecording();
87
return;
88
}
89
@@ -110,7 +105,7 @@ export default function Player({
105
const result = await invoke(TAURI_PLAYER_RECORD, {
106
audioId,
107
userId,
113
- playerVolumes: _playerVolumes(),
108
+ playerVolumes: currentPlayerVolumes(),
109
});
110
log.debug(TAURI_PLAYER_RECORD, result);
111
}, COUNTING_DOWN_NUMBER * 1000);
@@ -119,7 +114,7 @@ export default function Player({
114
}
115
};
116
122
- const _stopRecording = async () => {
117
+ const stopRecording = async () => {
118
setRecordingLength(0);
119
setPlayerState(PlayerState.STOPPED);
120
const stop_recording_result = await invoke(TAURI_PLAYER_RECORD_STOP, {
@@ -147,7 +142,7 @@ export default function Player({
142
log.error(error);
143
}
144
};
150
- const _playerVolumes = (): PlayerVolume[] => {
145
+ const currentPlayerVolumes = (): PlayerVolume[] => {
146
return [
147
{ mode: ModeDemucs.Vocals, volume: vocalsVolume },
148
{ mode: ModeDemucs.Drums, volume: drumsVolume },
@@ -156,7 +151,7 @@ export default function Player({
151
];
152
};
153
159
- const _togglePlayAudio = async () => {
154
+ const togglePlayAudio = async () => {
155
// Should disabled when recording
156
if (playerState === PlayerState.RECORDING) {
157
return;
@@ -170,7 +165,7 @@ export default function Player({
165
setIsCountingCountdown(false);
166
setPlayerState(PlayerState.PLAYING);
167
await invoke(TAURI_PLAYER_PLAY, {
173
- playerVolumes: _playerVolumes(),
168
+ playerVolumes: currentPlayerVolumes(),
169
});
170
}, COUNTING_DOWN_NUMBER * 1000);
171
break;
@@ -182,76 +177,28 @@ export default function Player({
177
case PlayerState.PAUSED:
178
log.debug(TAURI_PLAYER_RESUMED, "Resumed");
179
setPlayerState(PlayerState.PLAYING);
185
- await invoke(TAURI_PLAYER_PLAY, { playerVolumes: _playerVolumes() });
180
+ await invoke(TAURI_PLAYER_PLAY, { playerVolumes: currentPlayerVolumes() });
181
}
182
} catch (error) {
183
log.error(error);
184
}
185
};
191
-
192
- let buttonOrCounting = <></>;
193
- if (isCountingCountdown) {
194
- buttonOrCounting = <CountDownView seconds={COUNTING_DOWN_NUMBER} type="" />;
195
- } else {
196
- buttonOrCounting = (
197
- <ControlButtons
198
- isPlaying={playerState === PlayerState.PLAYING}
199
- isRecording={playerState === PlayerState.RECORDING}
200
- onClick={_togglePlayAudio}
201
- onStop={_stopPlayer}
202
- onRecord={_toggleRecording}
203
- />
204
- );
205
- }
206
-
207
- let audioWaveform = <></>;
208
- if (
209
- playerState === PlayerState.PLAYING ||
210
- playerState === PlayerState.PAUSED
211
- ) {
212
- audioWaveform = (
213
- <>
214
- <CountDownView seconds={recordingLength} type="Practicing!!!" />
215
- </>
216
- );
217
- } else if (playerState === PlayerState.RECORDING) {
218
- audioWaveform = (
219
- <RecordingView
220
- length={recordingLength}
221
- onRecordingEnd={onFinishedRecording}
222
- />
223
- );
224
- }
225
-
186
+
187
return (
227
- <div className="prose prose-sm prose-invert max-w-none">
228
- <div className="grid grid-rows-4 gap-6">
229
- {buttonOrCounting}
230
- <Row className="h-100 d-inline-block">
231
- <Col xs={12} className="mb-3 mt-3">
232
- <Container style={{ height: 300 }}>
233
- <div className="d-flex align-items-center justify-content-center h-100">
234
- <div className="d-flex flex-column">{audioWaveform}</div>
235
- </div>
236
- </Container>
237
- </Col>
238
- </Row>
239
- <Row className="border border-light">
240
- <Col xs={2} className="mb-3 mt-3">
241
- <p color="red">Volume</p>
242
- </Col>
243
- <Col xs={{ span: 2, offset: 8 }} className="mb-3 mt-3">
244
- <HideShowToggleView
245
- hideVolumeSliders={hideVolumeSliders}
246
- setHideVolumeSliders={setHideVolumeSliders}
247
- />
248
- </Col>
249
- <VolumeSlider
250
- _onVolumeChange={_onVolumeChange}
251
- isHidden={hideVolumeSliders}
252
- />
253
- </Row>
188
+ <>
189
+ <div className="bg-black border-slate-100 dark:bg-slate-800 dark:border-slate-500 border-b rounded-t-xl p-4 pb-6 sm:p-10 sm:pb-8 lg:p-6 xl:p-10 xl:pb-8 space-y-6 sm:space-y-8 lg:space-y-6 xl:space-y-8 items-center">
190
+ <AudioInfo />
191
+ <AudioInfoMiddle counterDownTimer={COUNTING_DOWN_NUMBER} />
192
+ <ControlButtons
193
+ isPlaying={playerState === PlayerState.PLAYING}
194
+ isRecording={playerState === PlayerState.RECORDING}
195
+ onClick={togglePlayAudio}
196
+ onStop={_stopPlayer}
197
+ onRecord={toggleRecording}
198
+ onResume={function (): void {
199
+ throw new Error("Function not implemented.");
200
+ } } />
201
</div>
255
- </div>
202
+ </>
203
);
204
}
frontend/splitfire-desktop/app/play/page.tsx
+12
-14
@@ -51,15 +51,15 @@ export default function Page() {
51
}
52
}
53
fetchData();
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
}, []);
56
-
56
+
57
// Sanity check
58
if (!audioFileId || !userId) {
59
log.error("Missing audio file ID or user ID");
60
return <div>Missing audio file ID</div>;
61
}
62
-
62
+
63
return (
64
<>
65
<div className="flex justify-between">
@@ -68,17 +68,15 @@ export default function Page() {
68
</div>
69
</div>
70
<div className="prose prose-sm prose-invert max-w-none">
71
- <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
72
- {state === State.LOADING && (
73
- <IconSpinner className="w-10 h-10 animate-spin" />
74
- )}
75
- {state === State.ERROR && (
76
- <div className="text-red-500">Failed to load song</div>
77
- )}
78
- {state === State.LOADED && (
79
- <Player audioId={audioFileId} userId={userId} />
80
- )}
81
- </div>
71
+ {state === State.LOADING && (
72
+ <IconSpinner className="w-10 h-10 animate-spin" />
73
+ )}
74
+ {state === State.ERROR && (
75
+ <div className="text-red-500">Failed to load song</div>
76
+ )}
77
+ {state === State.LOADED && (
78
+ <Player audioId={audioFileId} userId={userId} />
79
+ )}
80
</div>
81
</>
82
);