play song and add data to map item

This commit is contained in:
MathieuG-P
2022-11-11 02:43:30 +01:00
parent de86519282
commit 4223a17341
4 changed files with 138 additions and 14 deletions
@@ -47,14 +47,14 @@ export function LocalMapsListPanel({version, className} : Props) {
return <MapItem
key={map.hash}
hash={map.hash}
title={[map.rawInfo._songAuthorName, map.rawInfo._songName].join(" - ")}
title={map.rawInfo._songName}
coverUrl={map.coverUrl}
songUrl={map.songUrl}
autor={map.rawInfo._levelAuthorName}
songAutor={map.rawInfo._songAuthorName}
bpm={map.rawInfo._beatsPerMinute}
duration={null}
diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} qualified={null} ranked={null} autorId={map.bsaverInfo?.uploader?.id} plays={map.bsaverInfo?.stats?.plays} likes={map.bsaverInfo?.stats?.upvotes} createdAt={map.bsaverInfo?.createdAt}
duration={map.bsaverInfo?.metadata?.duration}
diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} qualified={null} ranked={null} autorId={map.bsaverInfo?.uploader?.id} likes={map.bsaverInfo?.stats?.upvotes} createdAt={map.bsaverInfo?.createdAt}
onDelete={() => {}}
/>;
}
@@ -4,10 +4,13 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmLink } from "../shared/bsm-link.component";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "../shared/bsm-button.component";
import { animate, motion } from "framer-motion";
import { motion } from "framer-motion";
import { useState } from "react";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import dateFormat from "dateformat";
import { AudioPlayerService } from "renderer/services/audio-player.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { map } from "rxjs/operators";
export type MapItemProps = {
hash: string,
@@ -24,25 +27,37 @@ export type MapItemProps = {
bpm: number,
duration: number,
likes: number,
plays: number,
createdAt: string,
onDelete?: (hash: string) => void,
onDownload?: (id: string) => void,
}
export function MapItem({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, plays, createdAt, onDelete, onDownload}: MapItemProps) {
export function MapItem({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, createdAt, onDelete, onDownload}: MapItemProps) {
const linkOpener = LinkOpenerService.getInstance();
const audioPlayer = AudioPlayerService.getInstance();
const color = useThemeColor("first-color");
const [hovered, setHovered] = useState(false);
const songPlaying = useObservable(audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl ? true : false)));
const zipUrl = `https://r2cdn.beatsaver.com/${hash}.zip`;
const previewUrl = `https://skystudioapps.com/bs-viewer/?url=${zipUrl}`;
const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null;
const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null;
const createdDate = createdAt ? dateFormat(createdAt, "d mmm yyyy") : null;
const likesText = likes ? Intl.NumberFormat(undefined, {notation: "compact"}).format(likes).split(" ").join("") : null;
const durationText = (() => {
if(!duration){ return null; }
const date = new Date(0);
date.setSeconds(duration);
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
})()
console.log(hash);
const openMapPage = () => {
if(mapUrl){
@@ -52,23 +67,51 @@ export function MapItem({hash, title, autor, songAutor, coverUrl, songUrl, autor
const openPreview = () => linkOpener.open(previewUrl, true);
const copyBsr = () => navigator.clipboard.writeText(`!bsr ${mapId}`);
const playSong = () => new Audio(songUrl).play();
const toogleMusic = () => {
if(songPlaying){
return audioPlayer.pause();
}
if(!audioPlayer.playing && audioPlayer.src === songUrl){
return audioPlayer.resume();
}
audioPlayer.play(songUrl, bpm);
}
return (
<motion.li className="h-[100px] min-w-[400px] shrink grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} onClick={openMapPage}>
<div className="h-full w-full relative pl-[90px] rounded-md overflow-hidden flex flex-row justify-end">
<div className="absolute top-0 left-0 h-full w-[100px]">
<div className="h-full w-full relative pl-[100px] rounded-md overflow-hidden flex flex-row justify-end">
<div className="absolute top-0 left-0 h-full aspect-square">
<BsmImage image={coverUrl}/>
<span className="absolute flex justify-center items-center w-full h-full bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40" style={{color}} onClick={(e) => {e.stopPropagation(); playSong()}}>
<span className="absolute flex justify-center items-center w-full h-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40" style={{color}} onClick={(e) => {e.stopPropagation(); toogleMusic()}}>
<BsmIcon className="w-full h-full p-7 opacity-0 group-hover:opacity-100 text-white hover:text-current" icon="play"/>
</span>
</div>
<div className="h-full w-full z-[1] relative rounded-md overflow-hidden">
<div className="h-full w-full z-[1] relative rounded-md overflow-hidden -translate-x-1">
<BsmImage className="absolute w-full h-full object-cover" image={coverUrl}/>
<div className="absolute py-1 pl-2 pr-7 top-0 left-0 w-full h-full bg-gray-600 bg-opacity-80 flex flex-col group-hover:bg-main-color-1 group-hover:bg-opacity-80">
<h1 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full leading-5 tracking-wide text-lg">{title}</h1>
<h2 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-sm mb-1">par {songAutor}</h2>
<h2 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-sm mb-[3px]">par {songAutor}</h2>
<h3 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-xs">mappée par <BsmLink href={authorUrl} className="brightness-150 saturate-50 hover:underline" style={{color}}>{autor}</BsmLink></h3>
<div className="w-full h-[15px] text-xs gap-2 hidden group-hover:flex">
{likesText && (
<div className="h-full flex items-center">
<BsmIcon className="h-[12px] w-fit shrink-0 mr-1" icon="trash"/>
<span className="mb-[2px]">{likesText}</span>
</div>
)}
{durationText && (
<div className="h-full flex items-center">
<BsmIcon className="h-[12px] w-fit shrink-0 mr-1" icon="trash"/>
<span className="mb-[2px]">{durationText}</span>
</div>
)}
{createdAt && (
<div className="h-full flex items-center">
<BsmIcon className="h-[12px] w-fit shrink-0 mr-1" icon="trash"/>
<time className="mb-[2px]">{createdDate}</time>
</div>
)}
</div>
</div>
</div>
<motion.div className="absolute bg-main-color-3 top-0 right-0 h-full z-[1]" initial={{width: "8px"}} animate={{width: hovered ? "30px" : "8px"}}>
@@ -1,13 +1,36 @@
import { memo } from "react"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { motion } from "framer-motion";
import { motion, Variants } from "framer-motion";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { AudioPlayerService } from "renderer/services/audio-player.service";
export const BsManagerIcon = memo(({className}: {className?: string}) => {
const audioPlayer = AudioPlayerService.getInstance();
const {firstColor, secondColor} = useThemeColor();
const playing= useObservable(audioPlayer.playing$);
const bpm = audioPlayer.bpm;
console.log("bpm", bpm, playing);
const transitions: Variants = {
playing: {
scale: [1, 1.05, 1],
transition: {repeat: Infinity, duration: ((60/bpm))/2, repeatDelay: ((60/bpm))/2}
},
idle: {}
}
const clickAction = () => {
if(playing){
audioPlayer.pause();
}
}
return (
<motion.div className="cursor-pointer" whileTap={{rotate: -10}}>
<motion.div className="cursor-pointer" whileTap={{rotate: -10}} variants={transitions} animate={playing && bpm > 0 ? "playing" : "idle"} onClick={clickAction}>
<svg className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<g fill={firstColor}>
<path d="M626.75,485.91l20.82,351.94a35.21,35.21,0,0,1-15.49,31.36L487.37,965.35l-.06,0A35.56,35.56,0,0,1,476,970.12a34.85,34.85,0,0,1-23.51-2.87L136.58,809.9a115.45,115.45,0,0,1-63.75-96.49L52,361.46a35.26,35.26,0,0,1,15.23-31.21l145.05-96.36a35.72,35.72,0,0,1,11.27-4.7,34.87,34.87,0,0,1,23.51,2.87L563,389.42a115.41,115.41,0,0,1,63.75,96.49Z"/>
@@ -0,0 +1,58 @@
import { Observable } from "rxjs";
import { BehaviorSubject } from "rxjs";
export class AudioPlayerService{
private static instance: AudioPlayerService;
public static getInstance(): AudioPlayerService{
if(!AudioPlayerService.instance){ AudioPlayerService.instance = new AudioPlayerService(); }
return AudioPlayerService.instance;
}
private readonly player: HTMLAudioElement;
private readonly _src$: BehaviorSubject<string> = new BehaviorSubject("");
private readonly _playing$: BehaviorSubject<boolean> = new BehaviorSubject(false);
private readonly _bpm$: BehaviorSubject<number> = new BehaviorSubject(0);
private constructor(){
this.player = new Audio();
this.player.onplay = () => this._playing$.next(true);
this.player.onpause = () => this._playing$.next(false);
this.player.onended = () => this._playing$.next(false);
}
public play(src: string, bpm = 0): Promise<void>{
this.pause();
this.player.src = src;
this._src$.next(src);
this._bpm$.next(bpm)
return this.player.play()
}
public pause(): void{
this._playing$.next(false);
this.player.pause();
}
public resume(): Promise<void>{
return this.player.play();
}
public get src$(): Observable<string>{
return this._src$.asObservable();
}
public get playing$(): Observable<boolean>{
return this._playing$.asObservable();
}
public get bpm$(): Observable<number>{
return this._bpm$.asObservable();
}
public get src(): string{ return this._src$.value; }
public get playing(): boolean{ return this._playing$.value; }
public get bpm(): number{ return this._bpm$.value; }
}