feat(Beatmaps): Add more info on beatmap card about each difficulty (#455)
This commit is contained in:
@@ -680,6 +680,68 @@ export default function({ name, color, width, height, style, secColor }) {
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case 'Circle':
|
||||
return (
|
||||
<svg viewBox="0 0 512 512" width={width} height={height}>
|
||||
<g>
|
||||
<path
|
||||
fill="rgb(255 255 255 / 28%)"
|
||||
d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 424c-97.06 0-176-79-176-176S158.94 80 256 80s176 79 176 176-78.94 176-176 176z"
|
||||
/>
|
||||
<path
|
||||
fill="rgb(255 255 255 / 75%)"
|
||||
d="M256 432c-97.06 0-176-79-176-176S158.94 80 256 80s176 79 176 176-78.94 176-176 176z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case 'Clock':
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 512 512">
|
||||
<g>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M347.216,301.211l-71.387-53.54V138.609c0-10.966-8.864-19.83-19.83-19.83c-10.966,0-19.83,8.864-19.83,19.83v118.978
|
||||
c0,6.246,2.935,12.136,7.932,15.864l79.318,59.489c3.569,2.677,7.734,3.966,11.878,3.966c6.048,0,11.997-2.717,15.884-7.952
|
||||
C357.766,320.208,355.981,307.775,347.216,301.211z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M256,0C114.833,0,0,114.833,0,256s114.833,256,256,256s256-114.833,256-256S397.167,0,256,0z M256,472.341
|
||||
c-119.275,0-216.341-97.066-216.341-216.341S136.725,39.659,256,39.659c119.295,0,216.341,97.066,216.341,216.341
|
||||
S375.275,472.341,256,472.341z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case 'Slider':
|
||||
return (
|
||||
<svg viewBox="0 0 368 368" width={width} height={height}>
|
||||
<g>
|
||||
<path
|
||||
fill={fill}
|
||||
d="M280,96H88c-48.52,0-88,39.48-88,88s39.48,88,88,88h192c48.52,0,88-39.48,88-88C368,135.48,328.52,96,280,96z M16,184
|
||||
c0-39.704,32.304-72,72-72s72,32.296,72,72s-32.304,72-72,72S16,223.704,16,184z M280,256H138.44
|
||||
c22.672-15.936,37.56-42.24,37.56-72s-14.888-56.064-37.56-72H280c39.696,0,72,32.296,72,72S319.696,256,280,256z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
case 'Spinner':
|
||||
return (
|
||||
<svg height={height} viewBox="-16 -18 533.33331 533" width={width}>
|
||||
<path
|
||||
fill={fill}
|
||||
d="m248.429688 25.082031c39.289062 0 78.640624 11.152344 113.800781 32.253907 22.300781 13.417968 42.339843 30.28125 59.363281 49.972656l-58.738281-1.9375c-6.898438-.230469-12.675781 5.179687-12.902344 12.082031-.226563 6.898437 5.179687 12.679687 12.082031 12.902344l81.269532 2.683593c4.667968 1.921876 10.035156.847657 13.605468-2.726562 3.566406-3.574219 4.632813-8.941406 2.703125-13.609375l-2.675781-81.25c-.15625-6.902344-5.878906-12.371094-12.78125-12.21875-6.898438.15625-12.375 5.878906-12.21875 12.777344 0 .09375 0 .179687.007812.269531l1.546876 46.875c-17.195313-18.375-36.847657-34.285156-58.398438-47.265625-39.042969-23.425781-82.84375-35.8085938-126.664062-35.8085938-70 0-134.539063 28.8632808-181.703126 81.2695308-41.78125 46.425782-66.726562 108.914063-66.726562 167.160157 0 6.902343 5.59375 12.5 12.5 12.5s12.5-5.597657 12.5-12.5c0-52.238281 22.542969-108.476563 60.308594-150.433594 42.367187-47.066406 100.292968-72.996094 163.121094-72.996094zm0 0"
|
||||
/>
|
||||
<path
|
||||
fill={fill}
|
||||
d="m487.5 236.011719c-6.90625 0-12.5 5.59375-12.5 12.5 0 52.234375-22.542969 108.476562-60.308594 150.4375-42.367187 47.066406-100.292968 72.988281-163.121094 72.988281-39.289062 0-78.640624-11.144531-113.800781-32.246094-22.300781-13.421875-42.339843-30.285156-59.363281-49.972656l58.738281 1.9375c6.898438.222656 12.675781-5.1875 12.902344-12.085938.226563-6.898437-5.179687-12.675781-12.082031-12.90625l-81.269532-2.679687c-4.667968-1.921875-10.035156-.84375-13.605468 2.726563-3.566406 3.574218-4.632813 8.9375-2.703125 13.605468l2.675781 81.25c.15625 6.90625 5.878906 12.378906 12.78125 12.222656 6.898438-.152343 12.375-5.875 12.21875-12.777343 0-.089844 0-.175781-.007812-.269531l-1.546876-46.875c17.195313 18.378906 36.847657 34.28125 58.398438 47.269531 39.042969 23.429687 82.84375 35.808593 126.664062 35.808593 70 0 134.539063-28.859374 181.703126-81.265624 41.78125-46.429688 66.726562-108.917969 66.726562-167.167969 0-6.90625-5.59375-12.5-12.5-12.5zm0 0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,39 @@
|
||||
export default timeStamp => {
|
||||
const timeSince = date => {
|
||||
const now = new Date();
|
||||
const secondsPast = (now.getTime() - timeStamp.getTime()) / 1000;
|
||||
const secondsPast = (now.getTime() - date.getTime()) / 1000;
|
||||
if (secondsPast < 60) {
|
||||
return `${parseInt(secondsPast, 10)} sec ago`;
|
||||
}
|
||||
if (secondsPast < 3600) {
|
||||
return `${parseInt(secondsPast / 60, 10)} min ago`;
|
||||
}
|
||||
if (secondsPast <= 86400) {
|
||||
return `${parseInt(secondsPast / 3600, 10)} h ago'`;
|
||||
if (secondsPast < 86400) {
|
||||
return `${parseInt(secondsPast / 3600, 10)} hours ago`;
|
||||
}
|
||||
const day = timeStamp.getDate();
|
||||
const month = timeStamp
|
||||
if (secondsPast < 2628336.2137829) {
|
||||
return `${parseInt(secondsPast / 86400, 10)} days ago`;
|
||||
}
|
||||
if (secondsPast < 31536000) {
|
||||
return `${parseInt(secondsPast / 2628336.2137829, 10)} months ago`;
|
||||
}
|
||||
const day = date.getDate();
|
||||
const month = date
|
||||
.toDateString()
|
||||
.match(/ [a-zA-Z]*/)[0]
|
||||
.replace(' ', '');
|
||||
const year = timeStamp.getFullYear() === now.getFullYear() ? '' : ` ${timeStamp.getFullYear()}`;
|
||||
const year = date.getFullYear() === now.getFullYear() ? '' : ` ${date.getFullYear()}`;
|
||||
return `${day} ${month + year}`;
|
||||
};
|
||||
|
||||
export default timeSince;
|
||||
|
||||
export const secToMinSec = sec => {
|
||||
const minutes = Math.floor(sec / 60);
|
||||
const seconds = sec - minutes * 60;
|
||||
|
||||
function str_pad_left(string, pad, length) {
|
||||
return (new Array(length + 1).join(pad) + string).slice(-length);
|
||||
}
|
||||
|
||||
return `${str_pad_left(minutes, '0', 2)}:${str_pad_left(seconds, '0', 2)}`;
|
||||
};
|
||||
|
||||
@@ -10,89 +10,128 @@ let makeBaseStyle = () =>
|
||||
alignItems(center),
|
||||
]);
|
||||
|
||||
let makeStatusStyle = status =>
|
||||
let makeStatusStyle = (status, difficulty) =>
|
||||
switch (status) {
|
||||
| "info" => style([backgroundColor(hex("3498db"))])
|
||||
| "pending" =>
|
||||
style([
|
||||
backgroundColor(rgba(241, 196, 15, 0.50)),
|
||||
borderColor(rgba(241, 196, 15, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(241, 196, 15, 0.7)),
|
||||
borderColor(rgb(241, 196, 15)),
|
||||
],
|
||||
backgroundColor(
|
||||
difficulty->Belt.Option.mapWithDefault(
|
||||
rgba(241, 196, 15),
|
||||
PpyHelpers.getBeatmapDifficultyColorRGBA,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
borderColor(
|
||||
difficulty->Belt.Option.mapWithDefault(
|
||||
rgba(241, 196, 15),
|
||||
PpyHelpers.getBeatmapDifficultyColorRGBA,
|
||||
0.8,
|
||||
),
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(241, 196, 15, 0.7)),
|
||||
borderColor(rgb(241, 196, 15)),
|
||||
]),
|
||||
])
|
||||
| "qualified" =>
|
||||
style([
|
||||
backgroundColor(rgba(241, 196, 15, 0.50)),
|
||||
borderColor(rgba(241, 196, 15, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(241, 196, 15, 0.7)),
|
||||
borderColor(rgb(241, 196, 15)),
|
||||
],
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(241, 196, 15, 0.7)),
|
||||
borderColor(rgb(241, 196, 15)),
|
||||
]),
|
||||
])
|
||||
| "graveyard" =>
|
||||
style([
|
||||
backgroundColor(rgba(231, 76, 60, 0.50)),
|
||||
borderColor(rgba(231, 76, 60, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(231, 76, 60, 0.7)),
|
||||
borderColor(rgb(231, 76, 60)),
|
||||
],
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(231, 76, 60, 0.7)),
|
||||
borderColor(rgb(231, 76, 60)),
|
||||
]),
|
||||
])
|
||||
| "WIP" =>
|
||||
style([
|
||||
backgroundColor(rgba(231, 76, 60, 0.50)),
|
||||
borderColor(rgba(231, 76, 60, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(231, 76, 60, 0.7)),
|
||||
borderColor(rgb(231, 76, 60)),
|
||||
],
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(231, 76, 60, 0.7)),
|
||||
borderColor(rgb(231, 76, 60)),
|
||||
]),
|
||||
])
|
||||
| "loved" =>
|
||||
style([
|
||||
backgroundColor(rgba(222, 90, 148, 0.50)),
|
||||
borderColor(rgba(222, 90, 148, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(222, 90, 148, 0.7)),
|
||||
borderColor(rgb(222, 90, 148)),
|
||||
],
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(222, 90, 148, 0.7)),
|
||||
borderColor(rgb(222, 90, 148)),
|
||||
]),
|
||||
])
|
||||
| "ranked" =>
|
||||
style([
|
||||
backgroundColor(rgba(46, 204, 113, 0.50)),
|
||||
borderColor(rgba(46, 204, 113, 0.8)),
|
||||
selector(
|
||||
"&:hover",
|
||||
[
|
||||
backgroundColor(rgba(46, 204, 113, 0.7)),
|
||||
borderColor(rgb(46, 204, 113)),
|
||||
],
|
||||
backgroundColor(
|
||||
difficulty->Belt.Option.mapWithDefault(
|
||||
rgba(46, 204, 113),
|
||||
PpyHelpers.getBeatmapDifficultyColorRGBA,
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
borderColor(
|
||||
difficulty->Belt.Option.mapWithDefault(
|
||||
rgba(46, 204, 113),
|
||||
PpyHelpers.getBeatmapDifficultyColorRGBA,
|
||||
0.8,
|
||||
),
|
||||
),
|
||||
hover([
|
||||
backgroundColor(rgba(46, 204, 113, 0.7)),
|
||||
borderColor(rgb(46, 204, 113)),
|
||||
]),
|
||||
])
|
||||
| _ => style([backgroundColor(hex("2c3e50"))])
|
||||
};
|
||||
|
||||
let makeStyle = status => [makeBaseStyle(), makeStatusStyle(status)]->merge;
|
||||
let makeStyle = (status, difficulty) =>
|
||||
[makeBaseStyle(), makeStatusStyle(status, difficulty)]->merge;
|
||||
|
||||
[@react.component]
|
||||
let make = (~status, ~style=?) => {
|
||||
<span className={makeStyle(status)} ?style>
|
||||
{status->String.capitalize_ascii->React.string}
|
||||
let make =
|
||||
(
|
||||
~status,
|
||||
~difficulty: option(float),
|
||||
~difficultyText: option(string),
|
||||
~style=?,
|
||||
) => {
|
||||
let (text, setText) = React.useState(() => status);
|
||||
|
||||
React.useEffect1(
|
||||
() => {
|
||||
setText(_ =>
|
||||
switch (difficultyText) {
|
||||
| Some(difficultyText) => difficultyText
|
||||
| None => status
|
||||
}
|
||||
);
|
||||
None;
|
||||
},
|
||||
[|difficultyText|],
|
||||
);
|
||||
|
||||
<span
|
||||
className={makeStyle(status, difficulty)}
|
||||
?style
|
||||
onMouseEnter={_ => setText(_ => status)}
|
||||
onMouseLeave={_ =>
|
||||
setText(_ =>
|
||||
switch (difficultyText) {
|
||||
| Some(difficultyText) => difficultyText
|
||||
| None => status
|
||||
}
|
||||
)
|
||||
}>
|
||||
<span> text->React.string </span>
|
||||
</span>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import React, { useState, useEffect, useCallback, useLayoutEffect } from 'react';
|
||||
import { createUseStyles } from 'react-jss';
|
||||
import renderIcons from '../../../helpers/renderIcons';
|
||||
import timeSince, { secToMinSec } from '../../../helpers/timeSince';
|
||||
import { make as Badge } from '../Badge.bs';
|
||||
import DifficultiesSelector from './DifficultiesSelector';
|
||||
|
||||
const totalPlayCount = beatmaps => beatmaps.reduce((playcount, beatmap) => playcount + beatmap.playcount, 0);
|
||||
const modes = mode => {
|
||||
const modesMap = Object.freeze({
|
||||
fruits: 'ctb',
|
||||
mania: 'mania',
|
||||
osu: 'std',
|
||||
taiko: 'taiko',
|
||||
});
|
||||
return modesMap[mode];
|
||||
};
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
wrapper: {
|
||||
backdropFilter: 'saturate(150%) blur(5px) brightness(0.5)',
|
||||
height: '100%',
|
||||
opacity: ({ isCardhovered }) => (isCardhovered ? 1 : 0),
|
||||
transition: 'opacity 150ms',
|
||||
borderTopLeftRadius: '5px',
|
||||
borderTopRightRadius: '5px',
|
||||
},
|
||||
topContainer: {
|
||||
display: 'inline-flex',
|
||||
width: '-webkit-fill-available',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
gap: '0.5rem',
|
||||
backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
padding: '.5rem',
|
||||
height: ({ isCardhovered }) => (isCardhovered ? '26px' : '0'),
|
||||
transitionDelay: '50ms',
|
||||
transition: 'height 200ms',
|
||||
},
|
||||
leftContainer: {
|
||||
position: 'absolute',
|
||||
top: '45px',
|
||||
left: '1rem',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'left',
|
||||
opacity: ({ isCardhovered }) => (isCardhovered ? 1 : 0),
|
||||
},
|
||||
rightContainer: {
|
||||
position: 'absolute',
|
||||
top: '45px',
|
||||
right: '1rem',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
textAlign: 'right',
|
||||
opacity: ({ noDiffSelectd }) => (noDiffSelectd ? 0 : 1),
|
||||
},
|
||||
versionTitle: {
|
||||
fontWeight: 600,
|
||||
fontSize: 'medium',
|
||||
opacity: ({ noDiffSelectd }) => (noDiffSelectd ? 0 : 1),
|
||||
},
|
||||
details: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
opacity: ({ noDiffSelectd }) => (noDiffSelectd ? 0 : 1),
|
||||
},
|
||||
one: {
|
||||
transitionDelay: '250ms',
|
||||
transitionDuration: '200ms',
|
||||
transitionProperty: 'opacity',
|
||||
},
|
||||
two: {
|
||||
transitionDelay: '50ms',
|
||||
transitionDuration: '200ms',
|
||||
transitionProperty: 'opacity',
|
||||
},
|
||||
three: {
|
||||
transitionDelay: '100ms',
|
||||
transitionDuration: '200ms',
|
||||
transitionProperty: 'opacity',
|
||||
},
|
||||
foor: {
|
||||
transitionDelay: '150ms',
|
||||
transitionDuration: '200ms',
|
||||
transitionProperty: 'opacity',
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
});
|
||||
|
||||
const BeatmapDetails = ({ beatmapSet, cardRef }) => {
|
||||
const [selectedDiff, setSelectedDiff] = useState('none');
|
||||
const [isCardhovered, setIsCardhovered] = useState(false);
|
||||
const noDiffSelectd = selectedDiff === 'none';
|
||||
const classes = useStyle({ noDiffSelectd, isCardhovered });
|
||||
const onDiffSelect = useCallback(diffIndex => setSelectedDiff(beatmapSet.beatmaps[diffIndex] ?? 'none'), []);
|
||||
useEffect(() => {
|
||||
if (cardRef.current) {
|
||||
const leaveHandler = () => {
|
||||
setSelectedDiff('none');
|
||||
setIsCardhovered(false);
|
||||
};
|
||||
const enterHandler = () => {
|
||||
setIsCardhovered(true);
|
||||
};
|
||||
cardRef.current.addEventListener('mouseleave', leaveHandler);
|
||||
cardRef.current.addEventListener('mouseenter', enterHandler);
|
||||
}
|
||||
}, [cardRef.current]);
|
||||
useLayoutEffect(() => {
|
||||
if (cardRef.current && selectedDiff !== 'none') {
|
||||
const matchingModePill = cardRef.current.querySelector(`img.pill.${modes(selectedDiff.mode)}`);
|
||||
matchingModePill.classList.add('highlight');
|
||||
cardRef.current.querySelector('div.availableModes').classList.add('hasHighlight');
|
||||
|
||||
return () => matchingModePill.classList.remove('highlight');
|
||||
}
|
||||
if (cardRef.current && selectedDiff === 'none') {
|
||||
cardRef.current.querySelector('div.availableModes').classList.remove('hasHighlight');
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [selectedDiff, cardRef.current]);
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.topContainer}>
|
||||
<Badge
|
||||
status={beatmapSet.status}
|
||||
difficulty={selectedDiff.difficulty}
|
||||
difficultyText={selectedDiff.difficulty && `☆ ${selectedDiff.difficulty}`}
|
||||
/>
|
||||
<p className={`${classes.versionTitle} ${classes.two}`}>{selectedDiff.version ?? 'Version'}</p>
|
||||
<p style={{ flexGrow: 1 }} />
|
||||
|
||||
<p className={`${classes.details} ${classes.foor}`} title="Circle count">
|
||||
{renderIcons({ name: 'Circle', width: '15px', height: '15px' })}
|
||||
<span>{selectedDiff.count_circles}</span>
|
||||
</p>
|
||||
<p className={`${classes.details} ${classes.three}`} title="Slider count">
|
||||
{renderIcons({ name: 'Slider', width: '15px', height: '15px' })}
|
||||
<span>{selectedDiff.count_sliders}</span>
|
||||
</p>
|
||||
<p className={`${classes.details} ${classes.two}`} title="Spinner count">
|
||||
{renderIcons({ name: 'Spinner', width: '14px', height: '14px' })}
|
||||
<span>{selectedDiff.count_spinners}</span>
|
||||
</p>
|
||||
<p
|
||||
className={`${classes.details} ${classes.one}`}
|
||||
style={{ opacity: isCardhovered ? 1 : 0 }}
|
||||
title="Duration in minutes"
|
||||
>
|
||||
{renderIcons({ name: 'Clock', width: '15px', height: '15px' })}
|
||||
<span>{secToMinSec(selectedDiff.total_length ?? beatmapSet.average_length)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${classes.leftContainer} ${classes.one}`}>
|
||||
<p title={new Date(beatmapSet.submitted_date).toLocaleString()}>
|
||||
<span>Submited </span>
|
||||
<span className={classes.bold}>{timeSince(new Date(beatmapSet.submitted_date))}</span>
|
||||
</p>
|
||||
<p title={new Date(beatmapSet.ranked_date).toLocaleString()}>
|
||||
<span>Ranked </span>
|
||||
<span className={classes.bold}>{timeSince(new Date(beatmapSet.ranked_date))}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Play count </span>
|
||||
<span className={classes.bold}>
|
||||
{noDiffSelectd ? totalPlayCount(beatmapSet.beatmaps) : selectedDiff.playcount}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>Favorite count </span>
|
||||
<span className={classes.bold}>{beatmapSet.favourite_count}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${classes.rightContainer} ${classes.two}`}>
|
||||
<p>
|
||||
<span className={classes.bold}>CS: </span>
|
||||
<span>{selectedDiff.cs}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className={classes.bold}>AR: </span>
|
||||
<span>{selectedDiff.ar}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className={classes.bold}>Drain: </span>
|
||||
<span>{selectedDiff.drain}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className={classes.bold}>Acc: </span>
|
||||
<span>{selectedDiff.accuracy}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DifficultiesSelector beatmaps={beatmapSet.beatmaps} onSelect={onDiffSelect} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BeatmapDetails;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import config from '../../../../shared/config';
|
||||
import BeatmapDetails from './BeatmapDetails';
|
||||
|
||||
const Cover = ({ url, width, height, paddingBottom, roundedTop, noFade, canLoad = true }) => {
|
||||
const Cover = ({ url, width, height, paddingBottom, roundedTop, noFade, canLoad = true, beatmapSet, parentRef }) => {
|
||||
const [loaded, isLoaded] = useState(false);
|
||||
const coverRef = useRef();
|
||||
useEffect(() => {
|
||||
@@ -29,7 +30,11 @@ const Cover = ({ url, width, height, paddingBottom, roundedTop, noFade, canLoad
|
||||
backgroundImage: loaded && `url('${url}')`,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
};
|
||||
return <div className="cover" style={style} />;
|
||||
return (
|
||||
<div className="cover" style={style}>
|
||||
<BeatmapDetails beatmapSet={beatmapSet} cardRef={parentRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Cover;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { createUseStyles } from 'react-jss';
|
||||
import { debounce } from 'underscore';
|
||||
import { getBeatmapDifficultyColorHex } from '../../../../shared/PpyHelpers.bs';
|
||||
|
||||
const DIFFICULTY_TRANSITION_DURATION = 160;
|
||||
const VERSION_TRANSITION_DURATION = 100;
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
wrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '25px',
|
||||
'&:hover': {
|
||||
'& > .diff': {
|
||||
height: '12px',
|
||||
},
|
||||
},
|
||||
transition: 'height 200ms',
|
||||
'& .prev': {
|
||||
height: '18px !important',
|
||||
},
|
||||
},
|
||||
difficulty: {
|
||||
height: '4px',
|
||||
flexBasis: '100%',
|
||||
boxSizing: 'border-box',
|
||||
alignSelf: 'flex-end',
|
||||
flexShrink: '2',
|
||||
minWidth: 0,
|
||||
'& > .version': {
|
||||
pointerEvents: 'none',
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
color: 'transparent',
|
||||
},
|
||||
'&:hover': {
|
||||
'& + .diff': {
|
||||
height: '18px',
|
||||
},
|
||||
flexShrink: '1',
|
||||
height: '22px !important',
|
||||
'& > .version': {
|
||||
transitionDelay: `${DIFFICULTY_TRANSITION_DURATION}ms`,
|
||||
transitionDuration: `${VERSION_TRANSITION_DURATION}ms`,
|
||||
transitionProperty: 'color',
|
||||
color: 'white',
|
||||
backdropFilter: 'brightness(0.9)',
|
||||
},
|
||||
},
|
||||
transition: `all ${DIFFICULTY_TRANSITION_DURATION}ms`,
|
||||
},
|
||||
});
|
||||
|
||||
const DifficultiesSelector = ({ beatmaps, onSelect }) => {
|
||||
const [selectedDiff, setSelectedDiff] = useState(-1);
|
||||
const debounceOnSelect = useCallback(debounce(onSelect, DIFFICULTY_TRANSITION_DURATION), []);
|
||||
useEffect(() => {
|
||||
if (selectedDiff > -1) debounceOnSelect(selectedDiff);
|
||||
else debounceOnSelect.cancel();
|
||||
}, [selectedDiff]);
|
||||
const classes = useStyle();
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
{beatmaps
|
||||
.sort((a, b) => a.difficulty - b.difficulty)
|
||||
.map((beatmap, i) => (
|
||||
<div
|
||||
onMouseLeave={() => {
|
||||
setSelectedDiff(-1);
|
||||
}}
|
||||
className={`${classes.difficulty} diff ${i === selectedDiff - 1 ? 'prev' : ''}`}
|
||||
style={{ backgroundColor: getBeatmapDifficultyColorHex(beatmap.difficulty) }}
|
||||
onMouseEnter={() => {
|
||||
setSelectedDiff(i);
|
||||
}}
|
||||
>
|
||||
<div className="version">{beatmap.version}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DifficultiesSelector;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { createUseStyles } from 'react-jss';
|
||||
import reqImgAssets from '../../../helpers/reqImgAssets';
|
||||
|
||||
const modePillsStyle = mode => ({
|
||||
width: 20,
|
||||
height: 20,
|
||||
margin: '3px',
|
||||
backgroundSize: 'contain',
|
||||
filter: 'brightness(0.85)',
|
||||
content: `url(${reqImgAssets(`./${mode}.png`)})`,
|
||||
});
|
||||
|
||||
const useStyle = createUseStyles({
|
||||
availableModes: {
|
||||
padding: '0 3px',
|
||||
display: 'inline-flex',
|
||||
'& > .pill': {
|
||||
transition: 'opacity 200ms ease',
|
||||
},
|
||||
'&.hasHighlight > .pill': {
|
||||
opacity: 0.15,
|
||||
},
|
||||
'&.hasHighlight > .pill.highlight': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Modes = ({ std, mania, taiko, ctb }) => {
|
||||
const classes = useStyle();
|
||||
return (
|
||||
<div
|
||||
className="rightContainer"
|
||||
style={{ position: 'absolute', right: '1%', bottom: '4%', display: 'inline-flex', margin: '0.2vw' }}
|
||||
>
|
||||
<div className={`${classes.availableModes} availableModes`} style={{ padding: '0 3px', display: 'inline-flex' }}>
|
||||
{std && <img alt="std" title="Standard" className="pill std" style={modePillsStyle('std')} />}
|
||||
{mania && <img alt="mania" title="Mania" className="pill mania" style={modePillsStyle('mania')} />}
|
||||
{taiko && <img alt="taiko" title="Taiko" className="pill taiko" style={modePillsStyle('taiko')} />}
|
||||
{ctb && <img alt="ctb" title="Catch The Beat" className="pill ctb" style={modePillsStyle('ctb')} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Modes.defaultProps = {
|
||||
std: false,
|
||||
mania: false,
|
||||
taiko: false,
|
||||
ctb: false,
|
||||
};
|
||||
|
||||
export default Modes;
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
import React, { useState, memo } from 'react';
|
||||
import React, { useState, memo, useRef } from 'react';
|
||||
import { shell } from 'electron';
|
||||
import { createUseStyles, useTheme } from 'react-jss';
|
||||
import Cover from './Cover';
|
||||
@@ -7,10 +7,9 @@ import DownloadBeatmapBtn from './DownloadBeatmapBtn';
|
||||
import PreviewBeatmapBtn from './PreviewBeatmapBtn';
|
||||
import renderIcons from '../../../helpers/renderIcons';
|
||||
import getBeatmapInfosUrl from '../../../helpers/getBeatmapInfosUrl';
|
||||
import { make as Badge } from '../Badge.bs';
|
||||
import reqImgAssets from '../../../helpers/reqImgAssets';
|
||||
import Button from '../Button';
|
||||
import SetWallpaperButton from './SetWallpaperButton';
|
||||
import Modes from './Modes';
|
||||
|
||||
const bpmToBps = bpm => 60 / bpm;
|
||||
|
||||
@@ -90,31 +89,24 @@ const useStyles = createUseStyles({
|
||||
export const getDownloadUrl = ({ id, unique_id }) => `https://beatconnect.io/b/${id}/${unique_id}`;
|
||||
|
||||
const Beatmap = ({ beatmap, noFade, autoDl, width, ...otherProps }) => {
|
||||
const ref = useRef();
|
||||
const theme = useTheme();
|
||||
const [isPlaying, setIsPLaying] = useState(false);
|
||||
const { beatmapset_id, id, title, artist, creator, version, bpm, beatconnectDlLink, beatmaps } = beatmap;
|
||||
const wallpaperBeatmapId = beatmaps[Math.max(beatmaps.length - 2, 0)].id;
|
||||
|
||||
const modePillsStyle = mode => ({
|
||||
width: 20,
|
||||
height: 20,
|
||||
margin: '3px',
|
||||
backgroundSize: 'contain',
|
||||
filter: 'brightness(0.85)',
|
||||
content: `url(${reqImgAssets(`./${mode}.png`)})`,
|
||||
});
|
||||
|
||||
const classes = useStyles({ width, theme, isPlaying, bpm: beatmap.bpm, ...otherProps });
|
||||
return (
|
||||
<div className={classes.Beatmap}>
|
||||
<div className={classes.Beatmap} ref={ref}>
|
||||
{beatmap && (
|
||||
<>
|
||||
{beatmap.status && <Badge style={{ position: 'absolute', top: '4%', right: '1%' }} status={beatmap.status} />}
|
||||
<Cover
|
||||
url={`https://assets.ppy.sh/beatmaps/${beatmapset_id || id}/covers/cover.jpg`}
|
||||
height={130}
|
||||
noFade={noFade}
|
||||
roundedTop
|
||||
beatmapSet={beatmap}
|
||||
parentRef={ref}
|
||||
/>
|
||||
<div className="leftContainer" style={{ position: 'absolute', left: '2%', bottom: '3%' }}>
|
||||
<p className={classes.Row}>
|
||||
@@ -153,25 +145,7 @@ const Beatmap = ({ beatmap, noFade, autoDl, width, ...otherProps }) => {
|
||||
{renderIcons({ name: 'Search', style: theme.accentContrast })}
|
||||
</Button>
|
||||
<SetWallpaperButton beatmapSetId={beatmapset_id || id} beatmapId={wallpaperBeatmapId} />
|
||||
<div
|
||||
className="rightContainer"
|
||||
style={{ position: 'absolute', right: '1%', bottom: '4%', display: 'inline-flex', margin: '0.2vw' }}
|
||||
>
|
||||
<div className="availableModes" style={{ padding: '0 3px', display: 'inline-flex' }}>
|
||||
{beatmap.mode_std && (
|
||||
<img alt="std" title="Standard" className="pill std" style={modePillsStyle('std')} />
|
||||
)}
|
||||
{beatmap.mode_mania && (
|
||||
<img alt="mania" title="Mania" className="pill mania" style={modePillsStyle('mania')} />
|
||||
)}
|
||||
{beatmap.mode_taiko && (
|
||||
<img alt="taiko" title="Taiko" className="pill taiko" style={modePillsStyle('taiko')} />
|
||||
)}
|
||||
{beatmap.mode_ctb && (
|
||||
<img alt="ctb" title="Catch The Beat" className="pill ctb" style={modePillsStyle('ctb')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Modes ctb={beatmap.mode_ctb} std={beatmap.mode_std} mania={beatmap.mode_mania} taiko={beatmap.mode_taiko} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"beatmaps": [
|
||||
{
|
||||
"id": 1650657,
|
||||
"mode": "osu",
|
||||
"mode": "fruits",
|
||||
"mode_int": 0,
|
||||
"difficulty": 5.04,
|
||||
"version": "Mofu Loli",
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
{
|
||||
"id": 1808794,
|
||||
"mode": "osu",
|
||||
"mode": "mania",
|
||||
"mode_int": 0,
|
||||
"difficulty": 2.32,
|
||||
"version": "Fresh Normal",
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"id": 2977441,
|
||||
"mode": "osu",
|
||||
"mode": "taiko",
|
||||
"mode_int": 0,
|
||||
"difficulty": 2.05,
|
||||
"version": "Fresh Easy",
|
||||
@@ -306,10 +306,10 @@
|
||||
"hype_required": null,
|
||||
"last_updated": "2021-06-02T16:45:11+00:00",
|
||||
"legacy_thread_url": "https://osu.ppy.sh/community/forums/topics/750701",
|
||||
"mode_ctb": false,
|
||||
"mode_mania": false,
|
||||
"mode_ctb": true,
|
||||
"mode_mania": true,
|
||||
"mode_std": true,
|
||||
"mode_taiko": false,
|
||||
"mode_taiko": true,
|
||||
"nominations_current": null,
|
||||
"nominations_required": null,
|
||||
"preview_url": "//b.ppy.sh/preview/786320.mp3",
|
||||
|
||||
@@ -21,3 +21,25 @@ let resolveThumbnail = (beatmapId, osuPath, fallbackUrl) => {
|
||||
|
||||
let resolveThumbURL = (beatmapId, osuPath) =>
|
||||
resolveThumbnail(beatmapId, osuPath, getListCoverUrl(beatmapId));
|
||||
|
||||
let getBeatmapDifficultyColorHex = (difficulty: float) => {
|
||||
switch (difficulty) {
|
||||
| difficulty when difficulty >= 6.5 => "#000"
|
||||
| difficulty when difficulty >= 5.3 => "#8866ee"
|
||||
| difficulty when difficulty >= 4.0 => "#ff66aa"
|
||||
| difficulty when difficulty >= 2.7 => "#ffcc22"
|
||||
| difficulty when difficulty >= 2.0 => "#66ccff"
|
||||
| _ => "#88b300"
|
||||
};
|
||||
};
|
||||
|
||||
let getBeatmapDifficultyColorRGBA = (difficulty: float) => {
|
||||
switch (difficulty) {
|
||||
| difficulty when difficulty >= 6.5 => Css.rgba(0, 0, 0)
|
||||
| difficulty when difficulty >= 5.3 => Css.rgba(136, 102, 238)
|
||||
| difficulty when difficulty >= 4.0 => Css.rgba(255, 102, 170)
|
||||
| difficulty when difficulty >= 2.7 => Css.rgba(255, 204, 34)
|
||||
| difficulty when difficulty >= 2.0 => Css.rgba(102, 204, 255)
|
||||
| _ => Css.rgba(136, 179, 0)
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user