forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1import styled from "@emotion/styled";
2import { Link as DefaultLink } from "@tanstack/react-router";
3import { ProgressBar } from "baseui/progress-bar";
4import { LabelSmall } from "baseui/typography";
5import { useRef } from "react";
6import { useTimeFormat } from "../../hooks/useFormat";
7import Equalizer from "../Icons/Equalizer";
8import Heart from "../Icons/Heart";
9import HeartOutline from "../Icons/HeartOutline";
10import Next from "../Icons/Next";
11import Pause from "../Icons/Pause";
12import Play from "../Icons/Play";
13import Playlist from "../Icons/Playlist";
14import Previous from "../Icons/Previous";
15import Speaker from "../Icons/Speaker";
16import {
17 Button,
18 Controls,
19 LikeButton,
20 MainWrapper,
21 NextButton,
22 PlayButton,
23 PreviousButton,
24 ProgressbarContainer,
25 RightActions,
26 styles,
27} from "./styles";
28
29const Container = styled.div`
30 position: fixed;
31 bottom: 0;
32 z-index: 1;
33 align-items: center;
34 display: flex;
35 height: 128px;
36`;
37
38const MiniPlayerWrapper = styled.div`
39 padding: 24px;
40`;
41
42const MiniPlayer = styled.div`
43 background-color: white;
44 width: 1120px;
45 height: 80px;
46 padding: 16px;
47 border-radius: 16px;
48 box-shadow: 0px 0px 24px rgba(19, 19, 19, 0.08);
49 display: flex;
50 flex-direction: row;
51 align-items: center;
52
53 @media (max-width: 1120px) {
54 width: 100vw;
55 }
56`;
57
58const Cover = styled.img`
59 width: 54px;
60 height: 54px;
61 margin-right: 16px;
62 border-radius: 5px;
63`;
64
65const Link = styled(DefaultLink)`
66 color: inherit;
67 text-decoration: none;
68 &:hover {
69 text-decoration: underline;
70 }
71`;
72
73export type StickyPlayerProps = {
74 nowPlaying?: {
75 title: string;
76 artist: string;
77 artistUri: string;
78 songUri: string;
79 albumUri: string;
80 duration: number;
81 progress: number;
82 albumArt?: string;
83 liked: boolean;
84 sha256: string;
85 } | null;
86 onPlay: () => void;
87 onPause: () => void;
88 onPrevious: () => void;
89 onNext: () => void;
90 onSpeaker: () => void;
91 onEqualizer: () => void;
92 onPlaylist: () => void;
93 onSeek: (position: number) => void;
94 onLike: (id: string) => void;
95 onDislike: (id: string) => void;
96 isPlaying: boolean;
97};
98
99function StickyPlayer(props: StickyPlayerProps) {
100 const {
101 nowPlaying,
102 onPlay,
103 onPause,
104 onPrevious,
105 onNext,
106 onSpeaker,
107 onEqualizer,
108 onPlaylist,
109 onSeek,
110 onLike,
111 onDislike,
112 isPlaying,
113 } = props;
114 const progressbarRef = useRef<HTMLDivElement>(null);
115 const { formatTime } = useTimeFormat();
116
117 // eslint-disable-next-line @typescript-eslint/no-explicit-any
118 const handleSeek = (e: any) => {
119 if (progressbarRef.current) {
120 const rect = progressbarRef.current.getBoundingClientRect();
121 const x = e.clientX - rect.left < 0 ? 0 : e.clientX - rect.left;
122 const width = rect.width;
123 const percentage = (x / width) * 100;
124 const time = (percentage / 100) * nowPlaying!.duration;
125 onSeek(Math.floor(time));
126 }
127 };
128
129 if (!nowPlaying) {
130 return <></>;
131 }
132
133 return (
134 <Container>
135 <MiniPlayerWrapper>
136 <MiniPlayer className="!bg-[var(--color-background)]">
137 {nowPlaying?.albumUri && (
138 <Link
139 to={`/${nowPlaying.albumUri.split("at://")[1].replace("app.rocksky.", "")}`}
140 >
141 <Cover src={nowPlaying?.albumArt} key={nowPlaying.albumUri} />
142 </Link>
143 )}
144 {!nowPlaying?.albumUri && (
145 <Cover src={nowPlaying?.albumArt} key={nowPlaying.albumUri} />
146 )}
147 <div className="max-w-[310px] overflow-hidden">
148 <div className="max-w-[310px] text-ellipsis overflow-hidden">
149 {!!nowPlaying?.songUri && (
150 <Link
151 to={`/${nowPlaying?.songUri?.split("at://")[1].replace("app.rocksky.", "")}`}
152 style={{
153 fontWeight: 600,
154 }}
155 className="text-ellipsis text-nowrap"
156 >
157 {nowPlaying?.title}
158 </Link>
159 )}
160 {!nowPlaying?.songUri && (
161 <div
162 style={{
163 fontWeight: 600,
164 }}
165 className="text-ellipsis text-nowrap"
166 >
167 {nowPlaying?.title}
168 </div>
169 )}
170 </div>
171 <div className="max-w-[310px] overflow-hidden text-ellipsis">
172 {!!nowPlaying?.artistUri && (
173 <Link
174 to={`/${nowPlaying?.artistUri?.split("at://")[1].replace("app.rocksky.", "")}`}
175 style={{
176 fontFamily: "RockfordSansLight",
177 fontWeight: 600,
178 }}
179 className="!text-[var(--color-text-muted)] text-ellipsis text-nowrap"
180 >
181 {nowPlaying?.artist}
182 </Link>
183 )}
184 {!nowPlaying?.artistUri && (
185 <div
186 style={{
187 fontFamily: "RockfordSansLight",
188 fontWeight: 600,
189 }}
190 className="text-[var(--color-text-muted)] text-ellipsis text-nowrap"
191 >
192 {nowPlaying?.artist}
193 </div>
194 )}
195 </div>
196 </div>
197 <div className="mt-[-14px] ml-[16px]">
198 <LikeButton
199 onClick={() => {
200 if (nowPlaying?.liked) {
201 onDislike(nowPlaying!.songUri);
202 return;
203 }
204 onLike(nowPlaying!.songUri);
205 }}
206 >
207 {nowPlaying?.liked && <Heart color="var(--color-primary)" />}
208 {!nowPlaying?.liked && <HeartOutline color="var(--color-text)" />}
209 </LikeButton>
210 </div>
211 <div className="ml-[16px]">
212 <div className="h-[45px] min-w-[43px]"></div>
213 <LabelSmall className="!text-[var(--color-text)] min-w-[43px]">
214 {formatTime(nowPlaying?.progress || 0)}
215 </LabelSmall>
216 </div>
217 <MainWrapper>
218 <Controls>
219 <PreviousButton onClick={onPrevious}>
220 <Previous color="var(--color-text)" />
221 </PreviousButton>
222 {!isPlaying && (
223 <PlayButton onClick={onPlay}>
224 <div className="mt-[5px] mr-[3px]">
225 <Play color="var(--color-text)" small />
226 </div>
227 </PlayButton>
228 )}
229 {isPlaying && (
230 <PlayButton onClick={onPause}>
231 <Pause color="var(--color-text)" small />
232 </PlayButton>
233 )}
234 <NextButton onClick={onNext}>
235 <Next color="var(--color-text)" />
236 </NextButton>
237 </Controls>
238 <div>
239 <ProgressbarContainer ref={progressbarRef} onClick={handleSeek}>
240 <ProgressBar
241 value={
242 nowPlaying?.progress && nowPlaying?.duration
243 ? (nowPlaying.progress / nowPlaying.duration) * 100
244 : 0
245 }
246 overrides={styles.Progressbar}
247 />
248 </ProgressbarContainer>
249 </div>
250 </MainWrapper>
251 <div className="mr-[16px]">
252 <div className="h-[45px]"></div>
253 <LabelSmall className="!text-[var(--color-text)]">
254 {formatTime(nowPlaying?.duration || 0)}
255 </LabelSmall>
256 </div>
257 <RightActions>
258 <Button
259 onClick={onSpeaker}
260 disabled
261 className="!bg-[var(--color-background)] !text-[var(--color-text)]"
262 >
263 <Speaker />
264 </Button>
265 <Button onClick={onEqualizer} disabled>
266 <Equalizer />
267 </Button>
268 <Button onClick={onPlaylist} disabled>
269 <Playlist />
270 </Button>
271 </RightActions>
272 </MiniPlayer>
273 </MiniPlayerWrapper>
274 </Container>
275 );
276}
277
278export default StickyPlayer;