A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at fix/spotify 278 lines 8.2 kB view raw
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;