[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: POC player

Based on https://codepen.io/keithpickering/pen/wjYJKM

FoxxMD (Sep 8, 2023, 1:40 PM EDT) 9c0f0299 f370b8ad

+841
+22
package-lock.json
··· 48 48 "react-redux": "^8.1.2", 49 49 "react-router-dom": "^6.15.0", 50 50 "safe-stable-stringify": "^1.1.1", 51 + "sass": "^1.66.1", 51 52 "spotify-web-api-node": "^5.0.2", 52 53 "superagent": "^8.0.9", 53 54 "tailwindcss": "^3.3.3", ··· 11896 11897 "url": "https://opencollective.com/immer" 11897 11898 } 11898 11899 }, 11900 + "node_modules/immutable": { 11901 + "version": "4.3.4", 11902 + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", 11903 + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" 11904 + }, 11899 11905 "node_modules/import-fresh": { 11900 11906 "version": "3.3.0", 11901 11907 "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", ··· 20792 20798 "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz", 20793 20799 "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==", 20794 20800 "dev": true 20801 + }, 20802 + "node_modules/sass": { 20803 + "version": "1.66.1", 20804 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", 20805 + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", 20806 + "dependencies": { 20807 + "chokidar": ">=3.0.0 <4.0.0", 20808 + "immutable": "^4.0.0", 20809 + "source-map-js": ">=0.6.2 <2.0.0" 20810 + }, 20811 + "bin": { 20812 + "sass": "sass.js" 20813 + }, 20814 + "engines": { 20815 + "node": ">=14.0.0" 20816 + } 20795 20817 }, 20796 20818 "node_modules/sass-loader": { 20797 20819 "version": "12.6.0",
+1
package.json
··· 85 85 "react-redux": "^8.1.2", 86 86 "react-router-dom": "^6.15.0", 87 87 "safe-stable-stringify": "^1.1.1", 88 + "sass": "^1.66.1", 88 89 "spotify-web-api-node": "^5.0.2", 89 90 "superagent": "^8.0.9", 90 91 "tailwindcss": "^3.3.3",
+340
src/client/components/player/Player.tsx
··· 1 + import React, {useState, useCallback} from 'react'; 2 + import './player.scss'; 3 + import PlayerTimestamp from "./PlayerTimestamp"; 4 + import PlayerControls from "./PlayerControls"; 5 + 6 + export interface PlayerProps { 7 + tracks: Track[] 8 + 9 + } 10 + 11 + export interface Track { 12 + name: string 13 + artist: string 14 + album: string 15 + year: number 16 + duration: number 17 + artwork: string 18 + } 19 + 20 + export const sampleTracks: Track[] = [ 21 + { 22 + name: "Somebody Hates Me", 23 + artist: "Reel Big Fish", 24 + album: "Why Do They Rock So Hard?", 25 + year: 1998, 26 + duration: 210, 27 + artwork: "https://is5-ssl.mzstatic.com/image/thumb/Music/59/db/b5/mzi.nloomwwj.jpg/268x0w.jpg" 28 + }, 29 + { 30 + name: "Beer", 31 + artist: "Reel Big Fish", 32 + album: "We're Not Happy Til You're Not Happy", 33 + year: 2005, 34 + duration: 233, 35 + artwork: "https://upload.wikimedia.org/wikipedia/en/9/9d/Reel_Big_Fish_-_We%27re_Not_Happy_%27til_You%27re_Not_Happy_cover.jpg" 36 + }, 37 + { 38 + name: "We Will Fall Together", 39 + artist: "Streetlight Manifesto", 40 + album: "Somewhere In The Between", 41 + year: 2007, 42 + duration: 289, 43 + artwork: "https://upload.wikimedia.org/wikipedia/en/9/95/Streetlight_Manifesto_-_Somewhere_in_the_Between.jpg" 44 + }, 45 + { 46 + name: "Welcome To The Jungle", 47 + artist: "Guns N' Roses", 48 + album: "Appetite For Destruction", 49 + year: 1987, 50 + duration: 277, 51 + artwork: "https://upload.wikimedia.org/wikipedia/en/6/60/GunsnRosesAppetiteforDestructionalbumcover.jpg" 52 + }, 53 + { 54 + name: "Estranged", 55 + artist: "Guns N' Roses", 56 + album: "Use Your Illusion II", 57 + year: 1991, 58 + duration: 563, 59 + artwork: "https://www.lifeofvinyl.com/images/super/0720642442012.jpg" 60 + }, 61 + { 62 + name: "Cheer Up!", 63 + artist: "Reel Big Fish", 64 + album: "Cheer Up!", 65 + year: 2002, 66 + duration: 163, 67 + artwork: "https://upload.wikimedia.org/wikipedia/en/f/f9/Reel_Big_Fish_-_Cheer_Up%21_cover.jpg" 68 + } 69 + ]; 70 + 71 + const Player = (props: PlayerProps) => { 72 + const [playStatus, setPlayStatus] = useState(false); 73 + const [currentTrack, setCurrentTrack] = useState(0); 74 + const [nextTrack, setNextTrack] = useState(1); 75 + const [prevTrack, setPrevTrack] = useState(props.tracks.length - 1); 76 + const [currentTime, setCurrentTime] = useState(0); 77 + const [sliding, setSliding] = useState(0); 78 + const [viewMode, setViewMode] = useState('player'); 79 + 80 + const toggleViewMode = useCallback(() => { 81 + let newViewMode = ""; 82 + switch(viewMode) { 83 + case "player": 84 + newViewMode = "playlist"; 85 + break; 86 + case "playlist": 87 + newViewMode = "player" 88 + break; 89 + } 90 + setViewMode(newViewMode); 91 + }, [viewMode, setViewMode]); 92 + 93 + const togglePlay = useCallback(() => {},[playStatus, setPlayStatus]); 94 + const changeTrack = useCallback((track) => {}, [props.tracks, nextTrack, prevTrack, setCurrentTrack, setSliding, setCurrentTrack, setNextTrack, setPrevTrack]); 95 + const toggleNextTrack = useCallback(() => {}, [nextTrack, changeTrack]); 96 + const togglePrevTrack = useCallback(() => {}, [prevTrack, changeTrack]); 97 + const setTime = useCallback((time) => {}, [props.tracks, currentTrack, setNextTrack, setCurrentTime]); 98 + 99 + const aPrevTrack = props.tracks[prevTrack]; 100 + const aCurrentTrack = props.tracks[currentTrack]; 101 + const aNextTrack = props.tracks[nextTrack]; 102 + 103 + let slidingStatus = ""; 104 + switch (sliding) { 105 + case -1: 106 + slidingStatus = "is-sliding-prev"; 107 + break; 108 + case 1: 109 + slidingStatus = "is-sliding-next"; 110 + break; 111 + } 112 + 113 + let playlistIcon = "fa fa-fw "; 114 + if (viewMode == "playlist") { 115 + playlistIcon += "fa-times"; 116 + } else { 117 + playlistIcon += "fa-bars"; 118 + } 119 + 120 + return ( 121 + <div className="wrapper"> 122 + <article className={["player", slidingStatus].join(' ')}> 123 + <div className="player__bg prev" style={{ backgroundImage: 'url(' + aPrevTrack.artwork + ')' }}></div> 124 + <div className="player__bg" style={{ backgroundImage: 'url(' + aCurrentTrack.artwork + ')' }}></div> 125 + <div className="player__bg next" style={{ backgroundImage: 'url(' + aNextTrack.artwork + ')' }}></div> 126 + <section className="player__art"> 127 + <img src={aPrevTrack.artwork} alt="" className="prev" /> 128 + <img src={aCurrentTrack.artwork} alt="" className="current" /> 129 + <img src={aNextTrack.artwork} alt="" className="next" /> 130 + 131 + {/*<button className="button toggle-playlist" onClick={toggleViewMode}> 132 + <span className="icon"> 133 + <i className={playlistIcon}></i> 134 + </span> 135 + </button>*/} 136 + 137 + {/*<Playlist tracks={this.props.tracks} isVisible={this.state.viewMode == "playlist"} changeTrack={this.changeTrack} />*/} 138 + </section> 139 + <section className="player__body"> 140 + <p className="title">{aCurrentTrack.name}</p> 141 + <p className="subtitle">{aCurrentTrack.artist}</p> 142 + <PlayerTimestamp duration={aCurrentTrack.duration} current={currentTime} /> 143 + </section> 144 + {/*<PlayerControls isPlaying={playStatus} isLiked={false} togglePlay={togglePlay} nextTrack={toggleNextTrack} prevTrack={togglePrevTrack} likeTrack={() => {}} />*/} 145 + </article> 146 + </div> 147 + ); 148 + } 149 + 150 + export default Player; 151 + 152 + /* 153 + export class PlayerC extends React.Component { 154 + static get defaultProps() { 155 + return { 156 + 157 + } 158 + } 159 + 160 + constructor(props, defaultProps) { 161 + super(props, defaultProps); 162 + 163 + this.state = { 164 + playStatus: 0, 165 + currentTrack: 0, 166 + nextTrack: 1, 167 + prevTrack: this.props.tracks.length-1, 168 + currentTime: 0, 169 + sliding: 0, 170 + likedTracks: [], 171 + viewMode: "player" 172 + } 173 + 174 + this.togglePlay = this.togglePlay.bind(this); 175 + this.toggleViewMode = this.toggleViewMode.bind(this); 176 + this.nextTrack = this.nextTrack.bind(this); 177 + this.prevTrack = this.prevTrack.bind(this); 178 + this.likeTrack = this.likeTrack.bind(this); 179 + } 180 + 181 + toggleViewMode() { 182 + let viewMode = ""; 183 + switch(this.state.viewMode) { 184 + case "player": 185 + viewMode = "playlist"; 186 + break; 187 + case "playlist": 188 + viewMode = "player" 189 + break; 190 + } 191 + 192 + this.setState({ 193 + viewMode: viewMode 194 + }); 195 + } 196 + 197 + togglePlay() { 198 + let status = this.state.playStatus; 199 + if (status == 0) { 200 + // Play 201 + status = 1; 202 + this.timer = setInterval(() => { 203 + this.setTime(this.state.currentTime + 1); 204 + }, 1000); 205 + } else { 206 + // Pause 207 + status = 0; 208 + clearInterval(this.timer); 209 + } 210 + this.setState({ playStatus: status }); 211 + } 212 + 213 + changeTrack(track, dir) { 214 + if (this.state.sliding == 0) { 215 + let this_track = track; 216 + let next_track = track + 1; 217 + let prev_track = track - 1; 218 + 219 + if (next_track >= this.props.tracks.length) next_track = 0; 220 + if (prev_track < 0) prev_track = this.props.tracks.length - 1; 221 + 222 + this.setState({ 223 + sliding: dir 224 + }); 225 + 226 + setTimeout(() => { 227 + this.setState({ 228 + sliding: 0, 229 + currentTrack: this_track, 230 + nextTrack: next_track, 231 + prevTrack: prev_track, 232 + currentTime: 0 233 + }); 234 + }, 500); 235 + } 236 + } 237 + 238 + nextTrack() { 239 + this.changeTrack(this.state.nextTrack, 1); 240 + } 241 + 242 + prevTrack() { 243 + if (this.state.currentTime < 2) { 244 + this.changeTrack(this.state.prevTrack, -1); 245 + } else { 246 + this.setState({ 247 + currentTime: 0 248 + }); 249 + } 250 + } 251 + 252 + likeTrack() { 253 + let likedTracks = this.state.likedTracks; 254 + let found = false; 255 + for (let i = 0; i < this.state.likedTracks.length; i++) { 256 + if (this.state.likedTracks[i] == this.state.currentTrack) { 257 + found = true; 258 + likedTracks.splice(i, 1); 259 + break; 260 + } 261 + } 262 + 263 + if (!found) { 264 + likedTracks.push(this.state.currentTrack); 265 + } 266 + 267 + this.setState({ 268 + likedTracks: likedTracks 269 + }); 270 + } 271 + 272 + setTime(time) { 273 + time = Math.floor(time); 274 + if (time > this.props.tracks[this.state.currentTrack].duration) { 275 + this.nextTrack(); 276 + } else { 277 + this.setState({ currentTime: time }); 278 + } 279 + } 280 + 281 + render() { 282 + const prevTrack = this.props.tracks[this.state.prevTrack]; 283 + const currentTrack = this.props.tracks[this.state.currentTrack]; 284 + const nextTrack = this.props.tracks[this.state.nextTrack]; 285 + 286 + let sliding = ""; 287 + switch (this.state.sliding) { 288 + case -1: 289 + sliding = "is-sliding-prev"; 290 + break; 291 + case 1: 292 + sliding = "is-sliding-next"; 293 + break; 294 + } 295 + 296 + let playlistIcon = "fa fa-fw "; 297 + if (this.state.viewMode == "playlist") { 298 + playlistIcon += "fa-times"; 299 + } else { 300 + playlistIcon += "fa-bars"; 301 + } 302 + 303 + let isLiked = false; 304 + for (let i = 0; i < this.state.likedTracks.length; i++) { 305 + if (this.state.likedTracks[i] == this.state.currentTrack) { 306 + isLiked = true; 307 + } 308 + } 309 + 310 + return ( 311 + <div className="wrapper"> 312 + <article className={["player", sliding].join(' ')}> 313 + <div className="player__bg prev" style={{ backgroundImage: 'url(' + prevTrack.artwork + ')' }}></div> 314 + <div className="player__bg" style={{ backgroundImage: 'url(' + currentTrack.artwork + ')' }}></div> 315 + <div className="player__bg next" style={{ backgroundImage: 'url(' + nextTrack.artwork + ')' }}></div> 316 + <section className="player__art"> 317 + <img src={prevTrack.artwork} alt="" className="prev" /> 318 + <img src={currentTrack.artwork} alt="" className="current" /> 319 + <img src={nextTrack.artwork} alt="" className="next" /> 320 + 321 + <button className="button toggle-playlist" onClick={this.toggleViewMode}> 322 + <span className="icon"> 323 + <i className={playlistIcon}></i> 324 + </span> 325 + </button> 326 + 327 + <Playlist tracks={this.props.tracks} isVisible={this.state.viewMode == "playlist"} changeTrack={this.changeTrack} /> 328 + </section> 329 + <section className="player__body"> 330 + <p className="title">{currentTrack.name}</p> 331 + <p className="subtitle">{currentTrack.artist}</p> 332 + <Timestamp duration={currentTrack.duration} current={this.state.currentTime} /> 333 + </section> 334 + <Controls isPlaying={this.state.playStatus} isLiked={isLiked} togglePlay={this.togglePlay} nextTrack={this.nextTrack} prevTrack={this.prevTrack} likeTrack={this.likeTrack} /> 335 + </article> 336 + </div> 337 + ); 338 + } 339 + } 340 + */
+111
src/client/components/player/PlayerControls.tsx
··· 1 + import React from "react"; 2 + import './controls.scss'; 3 + 4 + export interface ControlsProps { 5 + isPlaying: boolean 6 + isLiked: boolean 7 + prevTrack: () => any 8 + togglePlay: () => any 9 + nextTrack: () => any 10 + likeTrack: () => any 11 + } 12 + 13 + const Controls = (props: ControlsProps) => { 14 + let playClass = "fa fa-fw "; 15 + if (!props.isPlaying) { 16 + playClass += "fa-play"; 17 + } else { 18 + playClass += "fa-pause"; 19 + } 20 + 21 + let likeClass = "button like "; 22 + let heartClass = "fa fa-fw "; 23 + if (!props.isLiked) { 24 + heartClass += "fa-heart-o"; 25 + } else { 26 + likeClass += "is-liked"; 27 + heartClass += "fa-heart"; 28 + } 29 + 30 + return( 31 + <div className="controls"> 32 + <button type="button" className="button repeat"> 33 + <span className="icon"> 34 + <i className="fa fa-fw fa-repeat"></i> 35 + </span> 36 + </button> 37 + <button type="button" className="button prev" onClick={props.prevTrack}> 38 + <span className="icon"> 39 + <i className="fa fa-fw fa-step-backward"></i> 40 + </span> 41 + </button> 42 + <button type="button" className="button play" onClick={props.togglePlay}> 43 + <span className="icon"> 44 + <i className={playClass}></i> 45 + </span> 46 + </button> 47 + <button type="button" className="button next" onClick={props.nextTrack}> 48 + <span className="icon"> 49 + <i className="fa fa-fw fa-step-forward"></i> 50 + </span> 51 + </button> 52 + <button type="button" className={likeClass} onClick={props.likeTrack}> 53 + <span className="icon"> 54 + <i className={heartClass}></i> 55 + </span> 56 + </button> 57 + </div> 58 + ); 59 + } 60 + 61 + /*export class ControlsC extends React.Component { 62 + render() { 63 + let playClass = "fa fa-fw "; 64 + if (!this.props.isPlaying) { 65 + playClass += "fa-play"; 66 + } else { 67 + playClass += "fa-pause"; 68 + } 69 + 70 + let likeClass = "button like "; 71 + let heartClass = "fa fa-fw "; 72 + if (!this.props.isLiked) { 73 + heartClass += "fa-heart-o"; 74 + } else { 75 + likeClass += "is-liked"; 76 + heartClass += "fa-heart"; 77 + } 78 + 79 + return( 80 + <div className="controls"> 81 + <button type="button" className="button repeat"> 82 + <span className="icon"> 83 + <i className="fa fa-fw fa-repeat"></i> 84 + </span> 85 + </button> 86 + <button type="button" className="button prev" onClick={this.props.prevTrack}> 87 + <span className="icon"> 88 + <i className="fa fa-fw fa-step-backward"></i> 89 + </span> 90 + </button> 91 + <button type="button" className="button play" onClick={this.props.togglePlay}> 92 + <span className="icon"> 93 + <i className={playClass}></i> 94 + </span> 95 + </button> 96 + <button type="button" className="button next" onClick={this.props.nextTrack}> 97 + <span className="icon"> 98 + <i className="fa fa-fw fa-step-forward"></i> 99 + </span> 100 + </button> 101 + <button type="button" className={likeClass} onClick={this.props.likeTrack}> 102 + <span className="icon"> 103 + <i className={heartClass}></i> 104 + </span> 105 + </button> 106 + </div> 107 + ); 108 + } 109 + }*/ 110 + 111 + export default Controls;
+62
src/client/components/player/PlayerTimestamp.tsx
··· 1 + import React from "react"; 2 + import './timestamp.scss'; 3 + 4 + export interface TimestampProps { 5 + current: number 6 + duration: number 7 + } 8 + 9 + const convertTime = (rawTime: number) => { 10 + let mins = Math.floor(rawTime / 60); 11 + let seconds = rawTime - (mins * 60); 12 + let secStr: string = seconds.toString(); 13 + if (seconds < 10) { 14 + secStr = "0" + seconds; 15 + } 16 + return mins + ":" + secStr; 17 + } 18 + 19 + const Timestamp = (props: TimestampProps) => { 20 + return( 21 + <div className="timestamp"> 22 + <div className="timestamp__current"> 23 + {convertTime(props.current)} 24 + </div> 25 + <div className="timestamp__progress"> 26 + <div style={{ width: Math.floor((props.current / props.duration) * 100) + "%" }}></div> 27 + </div> 28 + <div className="timestamp__total"> 29 + {convertTime(props.duration - props.current)} 30 + </div> 31 + </div> 32 + ); 33 + } 34 + /*export class TimestampC extends React.Component { 35 + convertTime(time) { 36 + let mins = Math.floor(time / 60); 37 + let seconds = time - (mins * 60); 38 + if (seconds < 10) { 39 + seconds = "0" + seconds; 40 + } 41 + time = mins + ":" + seconds; 42 + return time; 43 + } 44 + 45 + render() { 46 + return( 47 + <div className="timestamp"> 48 + <div className="timestamp__current"> 49 + {this.convertTime(this.props.current)} 50 + </div> 51 + <div className="timestamp__progress"> 52 + <div style={{ width: Math.floor((this.props.current / this.props.duration) * 100) + "%" }}></div> 53 + </div> 54 + <div className="timestamp__total"> 55 + {this.convertTime(this.props.duration - this.props.current)} 56 + </div> 57 + </div> 58 + ); 59 + } 60 + }*/ 61 + 62 + export default Timestamp;
+33
src/client/components/player/Playlist.tsx
··· 1 + import React from 'react'; 2 + /*export class Playlist extends React.Component { 3 + render() { 4 + const tracks = this.props.tracks; 5 + 6 + let isHidden = ""; 7 + if (!this.props.isVisible) { 8 + isHidden = "is-hidden"; 9 + } 10 + 11 + return( 12 + <ul className={["playlist", isHidden].join(' ')}> 13 + {tracks.map((track, i) => { 14 + return( 15 + <li key={i} className="playlist__item media" onClick={() => this.props.changeTrack(i)}> 16 + <div className="media-left"> 17 + <p className="image is-48x48"> 18 + <img src={track.artwork} alt="" /> 19 + </p> 20 + </div> 21 + <div className="media-content"> 22 + <div className="content"> 23 + <p className="title">{track.name}</p> 24 + <p className="subtitle">{track.artist}</p> 25 + </div> 26 + </div> 27 + </li> 28 + ) 29 + })} 30 + </ul> 31 + ); 32 + } 33 + }*/
+1
src/client/components/player/README.md
··· 1 + Player designed based on [React Music Player UI](https://codepen.io/keithpickering/pen/wjYJKM) by [Keith Pickering](https://keithpickering.github.io/)
+40
src/client/components/player/controls.scss
··· 1 + @use './playerBase.scss'; 2 + $spacing: 1.5rem; 3 + $primary: #00ACC1; 4 + 5 + 6 + .controls { 7 + display: flex; 8 + justify-content: space-around; 9 + align-items: center; 10 + height: $spacing*2.5; 11 + text-align: center; 12 + background: $primary; 13 + 14 + .button { 15 + height: 100%; 16 + width: $spacing*2.5; 17 + border: 0; 18 + border-radius: 0; 19 + transition: background 250ms; 20 + box-shadow: none !important; 21 + background: transparent; 22 + color: white; 23 + 24 + &:hover { 25 + background: lighten($primary, 4%); 26 + } 27 + 28 + &.play { 29 + font-size: 1.6rem; 30 + } 31 + 32 + &.like.is-liked { 33 + background: #F06292; 34 + } 35 + 36 + .icon { 37 + //transition: transform 150ms; 38 + } 39 + } 40 + }
+113
src/client/components/player/player.scss
··· 1 + @use './playerBase.scss'; 2 + $spacing: 1.5rem; 3 + $primary: #00ACC1; 4 + 5 + 6 + .player { 7 + color: black; 8 + overflow: hidden; 9 + max-width: 90%; 10 + max-height: 90vh; 11 + //min-height: 487px; 12 + border-radius: 5px; 13 + background: white; 14 + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 15 + 16 + &__bg { 17 + position: fixed; 18 + top: -40px; 19 + right: -40px; 20 + bottom: -40px; 21 + left: -40px; 22 + background-size: cover; 23 + background-position: center; 24 + filter: blur(20px); 25 + z-index: -2; 26 + 27 + &:after { 28 + content: ""; 29 + position: absolute; 30 + top: -40px; 31 + right: -40px; 32 + bottom: -40px; 33 + left: -40px; 34 + background: rgba(#263238, 0.8); 35 + transition: background 1s; 36 + 37 + [class*="is-sliding"] & { 38 + background: rgba(#263238, 1); 39 + } 40 + } 41 + 42 + &.prev, 43 + &.next { 44 + opacity: 0; 45 + z-index: -1; 46 + } 47 + } 48 + 49 + &__head { 50 + padding: $spacing/2 $spacing; 51 + } 52 + 53 + &__art { 54 + position: relative; 55 + padding-bottom: 100%; 56 + width: 100%; 57 + border-bottom: 1px solid #ECEFF1; 58 + background: darken(#263238, 10%); 59 + 60 + .is-sliding-next & { 61 + transform: translate(-100%, 0); 62 + transition: transform 500ms ease-in-out; 63 + } 64 + 65 + .is-sliding-prev & { 66 + transform: translate(100%, 0); 67 + transition: transform 500ms ease-in-out; 68 + } 69 + 70 + > img { 71 + position: absolute; 72 + top: 0; 73 + left: 0; 74 + display: block; 75 + width: 100%; 76 + margin: 0 auto; 77 + 78 + &.prev { 79 + left: -100%; 80 + } 81 + 82 + &.next { 83 + left: 100%; 84 + } 85 + } 86 + } 87 + 88 + &__body { 89 + text-align: center; 90 + padding: $spacing; 91 + transition: all 250ms; 92 + 93 + [class*="is-sliding"] & { 94 + opacity: 0; 95 + transform: scale(0.95); 96 + } 97 + 98 + .title { 99 + position: relative; 100 + width: 100%; 101 + font-size: 1.2rem; 102 + white-space: nowrap; 103 + } 104 + 105 + .subtitle { 106 + font-size: 0.75rem; 107 + color: #90A4AE; 108 + padding-top: $spacing/4; 109 + margin-bottom: $spacing; 110 + text-transform: uppercase; 111 + } 112 + } 113 + }
+10
src/client/components/player/playerBase.scss
··· 1 + $spacing: 1.5rem; 2 + $primary: #00ACC1; 3 + 4 + .button { 5 + &:active { 6 + > .icon { 7 + transform: scale(0.85); 8 + } 9 + } 10 + }
+62
src/client/components/player/playlist.scss
··· 1 + $spacing: 1.5rem; 2 + $primary: #00ACC1; 3 + 4 + 5 + .playlist { 6 + position: absolute; 7 + top: 0; 8 + bottom: 0; 9 + left: 0; 10 + right: 0; 11 + padding: $spacing; 12 + padding-right: $spacing*2; 13 + overflow: auto; 14 + background: #263238; 15 + 16 + &__item.media { 17 + align-items: center; 18 + border-top: 2px solid #37474F; 19 + 20 + &:first-child { 21 + border-top: 0; 22 + } 23 + 24 + .title, 25 + .subtitle { 26 + white-space: nowrap; 27 + max-width: 100%; 28 + overflow: hidden; 29 + text-overflow: ellipsis; 30 + } 31 + 32 + .title { 33 + font-size: 0.85rem; 34 + color: #B0BEC5; 35 + } 36 + 37 + .subtitle { 38 + margin-top: -$spacing/2; 39 + font-size: 0.75rem; 40 + text-transform: uppercase; 41 + color: #607D8B 42 + } 43 + } 44 + } 45 + 46 + .toggle-playlist { 47 + position: absolute; 48 + top: $spacing/2; 49 + right: $spacing/2; 50 + border: 0; 51 + outline: 0; 52 + background: transparent; 53 + text-shadow: rgba(black, 0.75) 0 0 5px; 54 + box-shadow: none !important; 55 + color: white !important; 56 + z-index: 1; 57 + transition: all 500ms; 58 + 59 + [class*="is-sliding"] & { 60 + opacity: 0; 61 + } 62 + }
+46
src/client/components/player/timestamp.scss
··· 1 + @use './playerBase.scss'; 2 + $spacing: 1.5rem; 3 + $primary: #00ACC1; 4 + 5 + 6 + .timestamp { 7 + position: relative; 8 + width: 100%; 9 + height: $spacing; 10 + 11 + &__progress, 12 + &__current, 13 + &__total { 14 + position: absolute; 15 + top: 0; 16 + font-size: 0.75rem; 17 + } 18 + 19 + &__progress { 20 + position: relative; 21 + top: $spacing/4; 22 + left: 50%; 23 + height: $spacing/4; 24 + width: calc(100% - #{$spacing*3}); 25 + transform: translate(-50%, 0); 26 + border-radius: 5px; 27 + overflow: hidden; 28 + background: #ECEFF1; 29 + 30 + > div { 31 + position: absolute; 32 + top: 0; 33 + left: 0; 34 + bottom: 0; 35 + background: $primary; 36 + } 37 + } 38 + 39 + &__current { 40 + left: 0; 41 + } 42 + 43 + &__total { 44 + right: 0; 45 + } 46 + }