mirror of
https://github.com/generativefm/generative.fm.git
synced 2026-01-08 22:38:01 -05:00
Add favorite and more buttons to currently playing area
This commit is contained in:
2
.babelrc
2
.babelrc
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"presets": ["react", "env"]
|
||||
"presets": ["@babel/preset-react", "@babel/preset-env"]
|
||||
}
|
||||
|
||||
2037
package-lock.json
generated
2037
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -25,12 +25,12 @@
|
||||
},
|
||||
"homepage": "https://github.com/metalex9/generative-music#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"aws-sdk": "^2.420.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^2.1.1",
|
||||
@@ -49,6 +49,8 @@
|
||||
"offline-plugin": "^5.0.6",
|
||||
"prettier": "^1.16.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"sass-loader": "^7.1.0",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.29.6",
|
||||
@@ -88,13 +90,12 @@
|
||||
"@generative-music/piece-townsend": "^2.12.0",
|
||||
"@generative-music/piece-trees": "^2.12.0",
|
||||
"@generative-music/samples.generative.fm": "^1.11.1",
|
||||
"@generative-music/visualizer": "^1.0.0",
|
||||
"audiobuffer-to-wav": "^1.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"clone": "^2.1.2",
|
||||
"nice-color-palettes": "^2.0.0",
|
||||
"react": "^16.8.4",
|
||||
"react-device-detect": "^1.6.2",
|
||||
"react-dom": "^16.8.4",
|
||||
"react-redux": "^5.1.1",
|
||||
"react-render-html": "^0.6.0",
|
||||
"react-router": "^4.3.1",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.control-button {
|
||||
width: $buttonWidth;
|
||||
height: $buttonWidth;
|
||||
background: $backgroundColor;
|
||||
background: transparent;
|
||||
border: none;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -7,7 +7,7 @@ $sideControlWidth: 33%;
|
||||
background: $backgroundColor;
|
||||
height: $controlsHeight;
|
||||
width: 100%;
|
||||
border-top: 1px solid $primaryColor;
|
||||
border-top: 1px solid $secondaryColor;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import '_colors.scss';
|
||||
@import '_variables.scss';
|
||||
|
||||
@mixin textEllipsisOverflow {
|
||||
overflow: hidden;
|
||||
@@ -13,16 +14,29 @@
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
font-size: 0.9em;
|
||||
|
||||
&__title {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
color: $primaryColor;
|
||||
@include textEllipsisOverflow;
|
||||
}
|
||||
&__artist {
|
||||
&__btns {
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.8em;
|
||||
@include textEllipsisOverflow;
|
||||
|
||||
&__btn {
|
||||
margin: 0.2em 0.5em 0;
|
||||
font-size: 1.25em;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__visualizer {
|
||||
max-height: $controlsHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,64 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import VisualizerContainer from '@containers/visualizer.container';
|
||||
import { react, animators } from '@generative-music/visualizer';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faHeart, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
|
||||
import pieces from '@pieces';
|
||||
import artists from '@data/artists';
|
||||
import FavoriteButton from '@containers/favorite-button.container';
|
||||
import MoreButton from '@components/shared/more-button';
|
||||
import './currently-playing.scss';
|
||||
|
||||
const CurrentlyPlayingComponent = ({ selectedPieceId }) => {
|
||||
const { Static, Animated } = react;
|
||||
|
||||
const CurrentlyPlayingComponent = ({ selectedPieceId, isPlaying }) => {
|
||||
const hasSelection = selectedPieceId !== null;
|
||||
const { artist, title } = hasSelection
|
||||
? pieces.find(({ id }) => id === selectedPieceId)
|
||||
: { artist: '', title: '' };
|
||||
const containerRef = useRef(null);
|
||||
const [animator, setAnimator] = useState(null);
|
||||
const [height, setHeight] = useState(null);
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
setAnimator(
|
||||
animators.makeEndlessAnimator({
|
||||
animationDuration: 30 * 1000,
|
||||
now: Date.now,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setAnimator(null);
|
||||
}
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeight(containerRef.current.clientHeight);
|
||||
}, [containerRef]);
|
||||
|
||||
const visualizer =
|
||||
isPlaying && animator !== null ? (
|
||||
<Animated width={height} height={height} animator={animator} />
|
||||
) : (
|
||||
<Static width={height} height={height} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="currently-playing">
|
||||
<div className="currently-playing" ref={containerRef}>
|
||||
<div className="currently-playing__visualizer">
|
||||
{hasSelection && <VisualizerContainer />}
|
||||
{hasSelection && visualizer}
|
||||
</div>
|
||||
<div className="currently-playing__info">
|
||||
<div className="currently-playing__info__title">{title}</div>
|
||||
<div className="currently-playing__info__artist">
|
||||
{artist !== '' ? artists[artist] : artist}
|
||||
<div className="currently-playing__info__btns">
|
||||
<FavoriteButton
|
||||
className="currently-playing__info__btns__btn"
|
||||
pieceId={selectedPieceId}
|
||||
/>
|
||||
<MoreButton
|
||||
className="currently-playing__info__btns__btn"
|
||||
pieceId={selectedPieceId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,6 +67,7 @@ const CurrentlyPlayingComponent = ({ selectedPieceId }) => {
|
||||
|
||||
CurrentlyPlayingComponent.propTypes = {
|
||||
selectedPieceId: propTypes.string,
|
||||
isPlaying: propTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CurrentlyPlayingComponent;
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import svg from 'svg.js';
|
||||
import colorSchemes from 'nice-color-palettes/1000';
|
||||
|
||||
const createVisualization = containerElement => {
|
||||
const TWO = 2;
|
||||
const STROKE_WIDTH_DIVISOR = 100;
|
||||
const LENGTH = 55;
|
||||
const STROKE_WIDTH = Math.max(LENGTH / STROKE_WIDTH_DIVISOR, 1);
|
||||
const DIAGONAL = Math.ceil(
|
||||
Math.sqrt(TWO * LENGTH * LENGTH) + TWO * STROKE_WIDTH
|
||||
);
|
||||
const HALF_DIAGONAL = DIAGONAL / TWO;
|
||||
const HALF_LENGTH = LENGTH / TWO;
|
||||
const HALF_STROKE_WIDTH = STROKE_WIDTH / TWO;
|
||||
const ANIMATION_TIME_MS = 10000;
|
||||
const TRI_CLIP_SIZE = HALF_LENGTH + STROKE_WIDTH;
|
||||
const P_ROTATE = 0.0375;
|
||||
const P_RECT = 0.4;
|
||||
const P_SQR = 0.2;
|
||||
const P_TRI = 0.1;
|
||||
const P_50_PCT = 0.5;
|
||||
const P_FILL = P_50_PCT;
|
||||
const P_FILL_BLACK = P_50_PCT;
|
||||
const ROTATION_ANGLE = 90;
|
||||
const INTERVAL_TIME_MS = ANIMATION_TIME_MS * TWO;
|
||||
const OFFSET = (DIAGONAL - LENGTH) / TWO;
|
||||
|
||||
const getRandomElement = arr => arr[Math.floor(Math.random() * arr.length)];
|
||||
const getRandomColorScheme = () => getRandomElement(colorSchemes);
|
||||
|
||||
let colors;
|
||||
|
||||
const setNewColorScheme = () => {
|
||||
colors = getRandomColorScheme();
|
||||
};
|
||||
|
||||
setNewColorScheme();
|
||||
|
||||
const getRandomColor = () =>
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
const draw = svg(containerElement).size(DIAGONAL, DIAGONAL);
|
||||
|
||||
const stroke = el =>
|
||||
el.attr({
|
||||
stroke: 'black',
|
||||
'stroke-width': STROKE_WIDTH,
|
||||
'fill-opacity': 0,
|
||||
});
|
||||
|
||||
const strokeAndFill = (el, fill = getRandomColor()) =>
|
||||
el.attr({
|
||||
'fill-opacity': 1,
|
||||
stroke: 'black',
|
||||
'stroke-width': STROKE_WIDTH,
|
||||
fill,
|
||||
});
|
||||
|
||||
const shape = el =>
|
||||
el
|
||||
.attr({
|
||||
'fill-opacity': 0,
|
||||
})
|
||||
.dmove(OFFSET, OFFSET);
|
||||
|
||||
const rect = (w, h) => shape(draw.rect(w, h));
|
||||
|
||||
const triClip = sqr =>
|
||||
sqr
|
||||
.clone()
|
||||
.size(TRI_CLIP_SIZE, TRI_CLIP_SIZE)
|
||||
.dmove(-HALF_STROKE_WIDTH, -HALF_STROKE_WIDTH);
|
||||
|
||||
const tri = (x1, y1, x2, y2, clipper) =>
|
||||
shape(
|
||||
draw.polygon(`${x1},${y1} ${x2},${y2} ${HALF_LENGTH},${HALF_LENGTH}`)
|
||||
).clipWith(clipper);
|
||||
|
||||
const container = rect(LENGTH, LENGTH);
|
||||
stroke(container);
|
||||
|
||||
const tRect = rect(LENGTH, HALF_LENGTH);
|
||||
const bRect = rect(LENGTH, HALF_LENGTH).dmove(0, HALF_LENGTH);
|
||||
const lRect = rect(HALF_LENGTH, LENGTH);
|
||||
const rRect = rect(HALF_LENGTH, LENGTH).dmove(HALF_LENGTH, 0);
|
||||
const sqrTl = rect(HALF_LENGTH, HALF_LENGTH);
|
||||
const sqrTr = rect(HALF_LENGTH, HALF_LENGTH).dmove(HALF_LENGTH, 0);
|
||||
const sqrBl = rect(HALF_LENGTH, HALF_LENGTH).dmove(0, HALF_LENGTH);
|
||||
const sqrBr = rect(HALF_LENGTH, HALF_LENGTH).dmove(HALF_LENGTH, HALF_LENGTH);
|
||||
|
||||
const tlClipper = draw.clip().add(triClip(sqrTl));
|
||||
const t1 = tri(0, 0, 0, HALF_LENGTH, tlClipper);
|
||||
const t2 = tri(0, 0, HALF_LENGTH, 0, tlClipper);
|
||||
const trClipper = draw.clip().add(triClip(sqrTr));
|
||||
const t3 = tri(HALF_LENGTH, 0, LENGTH, 0, trClipper);
|
||||
const t4 = tri(LENGTH, 0, LENGTH, HALF_LENGTH, trClipper);
|
||||
const blClipper = draw.clip().add(triClip(sqrBl));
|
||||
const t5 = tri(0, HALF_LENGTH, 0, LENGTH, blClipper);
|
||||
const t6 = tri(0, LENGTH, HALF_LENGTH, LENGTH, blClipper);
|
||||
const brClipper = draw.clip().add(triClip(sqrBr));
|
||||
const t7 = tri(HALF_LENGTH, LENGTH, LENGTH, LENGTH, brClipper);
|
||||
const t8 = tri(LENGTH, LENGTH, LENGTH, HALF_LENGTH, brClipper);
|
||||
|
||||
const rectangles = [tRect, bRect, lRect, rRect];
|
||||
const squares = [sqrTl, sqrTr, sqrBl, sqrBr];
|
||||
const triangles = [t1, t2, t3, t4, t5, t6, t7, t8];
|
||||
|
||||
const allShapes = rectangles.concat(squares).concat(triangles);
|
||||
|
||||
const group = draw.group();
|
||||
allShapes
|
||||
.concat([container, tlClipper, trClipper, blClipper, brClipper])
|
||||
.forEach(s => {
|
||||
group.add(s);
|
||||
});
|
||||
|
||||
const makeNext = (animate = true, changeScheme = false) => {
|
||||
if (changeScheme) {
|
||||
setNewColorScheme();
|
||||
}
|
||||
allShapes.concat([group]).forEach(s => s.finish());
|
||||
if (Math.random() >= P_ROTATE || !animate) {
|
||||
[[rectangles, P_RECT], [squares, P_SQR], [triangles, P_TRI]].forEach(
|
||||
([shapes, p]) => {
|
||||
shapes.forEach(s => {
|
||||
const el = animate ? s.animate(ANIMATION_TIME_MS, '-') : s;
|
||||
if (Math.random() < p) {
|
||||
if (Math.random() < P_FILL) {
|
||||
stroke(el);
|
||||
} else if (Math.random() < P_FILL_BLACK) {
|
||||
strokeAndFill(el, '#000');
|
||||
} else {
|
||||
strokeAndFill(el);
|
||||
}
|
||||
} else {
|
||||
el.attr({
|
||||
'fill-opacity': 0,
|
||||
'stroke-width': 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const { rotation } = group.transform();
|
||||
const dRotation =
|
||||
Math.random() < P_50_PCT ? -ROTATION_ANGLE : ROTATION_ANGLE;
|
||||
group
|
||||
.animate(ANIMATION_TIME_MS, '-')
|
||||
.rotate(rotation + dRotation, HALF_DIAGONAL, HALF_DIAGONAL);
|
||||
setNewColorScheme();
|
||||
}
|
||||
};
|
||||
|
||||
let interval;
|
||||
|
||||
return {
|
||||
start: () => {
|
||||
makeNext();
|
||||
interval = setInterval(() => makeNext(), INTERVAL_TIME_MS);
|
||||
},
|
||||
stop: () => {
|
||||
allShapes.concat([group]).forEach(s => s.finish());
|
||||
allShapes.forEach(s => {
|
||||
s.attr({
|
||||
'fill-opacity': 0,
|
||||
'stroke-width': 0,
|
||||
});
|
||||
});
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createVisualization;
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, { Component, createRef } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import createVisualization from './create-visualization';
|
||||
import './visualizer.scss';
|
||||
|
||||
class VisualizerComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.containerElement = createRef();
|
||||
}
|
||||
render() {
|
||||
return <div className="visualizer" ref={this.containerElement} />;
|
||||
}
|
||||
componentDidMount() {
|
||||
const visualization = createVisualization(this.containerElement.current);
|
||||
if (this.props.isPlaying) {
|
||||
visualization.start();
|
||||
}
|
||||
this.setState({
|
||||
visualization,
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.state.visualization.stop();
|
||||
}
|
||||
componentDidUpdate(previousProps) {
|
||||
if (previousProps.isPlaying && !this.props.isPlaying) {
|
||||
this.state.visualization.stop();
|
||||
} else if (!previousProps.isPlaying && this.props.isPlaying) {
|
||||
this.state.visualization.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VisualizerComponent.propTypes = {
|
||||
isPlaying: propTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default VisualizerComponent;
|
||||
@@ -1,7 +0,0 @@
|
||||
@import '_colors.scss';
|
||||
|
||||
.visualizer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -28,13 +28,14 @@ const getTimerIcon = ({ lastDurationsMS, remainingMS }) => {
|
||||
return faHourglassEnd;
|
||||
};
|
||||
|
||||
const makePrimaryButton = (faIcon, onClick) =>
|
||||
const makePrimaryButton = (faIcon, onClick, title) =>
|
||||
function PrimaryButtonComponent() {
|
||||
return (
|
||||
<ControlButtonComponent
|
||||
faIcon={faIcon}
|
||||
onClick={onClick}
|
||||
isPrimary={true}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -61,8 +62,8 @@ const MainControlsComponent = ({
|
||||
setIsPopoverOpen(val => !val);
|
||||
};
|
||||
const PrimaryButtonComponent = isPlaying
|
||||
? makePrimaryButton(faStop, onStopClick)
|
||||
: makePrimaryButton(faPlay, onPlayClick);
|
||||
? makePrimaryButton(faStop, onStopClick, 'Stop')
|
||||
: makePrimaryButton(faPlay, onPlayClick, 'Play');
|
||||
|
||||
const isTimerButton = el =>
|
||||
el &&
|
||||
@@ -94,19 +95,26 @@ const MainControlsComponent = ({
|
||||
faIcon={getTimerIcon(timer)}
|
||||
onClick={toggleIsPopoverOpen}
|
||||
isActive={timer.remainingMS > 0}
|
||||
title="Play timer..."
|
||||
/>
|
||||
</Popover>
|
||||
<ControlButtonComponent
|
||||
faIcon={faStepBackward}
|
||||
onClick={onPreviousClick}
|
||||
title="Previous piece"
|
||||
/>
|
||||
<PrimaryButtonComponent />
|
||||
<ControlButtonComponent faIcon={faStepForward} onClick={onNextClick} />
|
||||
<ControlButtonComponent
|
||||
faIcon={faStepForward}
|
||||
onClick={onNextClick}
|
||||
title="Next piece"
|
||||
/>
|
||||
{!isMobile && (
|
||||
<ControlButtonComponent
|
||||
faIcon={faRandom}
|
||||
onClick={isShuffleActive ? disableShuffle : enableShuffle}
|
||||
isActive={isShuffleActive}
|
||||
title={`${isShuffleActive ? 'Disable' : 'Enable'} shuffle`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
|
||||
type="button"
|
||||
className="timer-box__durations__item__btn"
|
||||
onClick={() => handleDurationSelect(durationMS)}
|
||||
title={`Start ${durationMS / 60000}-minute timer`}
|
||||
>{`${durationMS / 60000} minutes`}</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -33,6 +34,7 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
|
||||
className="timer-box__input"
|
||||
value={customDuration}
|
||||
onChange={event => setCustomDuration(event.target.value)}
|
||||
title="Timer duration in minutes"
|
||||
/>
|
||||
minutes
|
||||
<button
|
||||
@@ -40,6 +42,11 @@ const StartTimerContent = ({ lastDurationsMS, startTimer }) => {
|
||||
className="timer-box__btn timer-box__btn--inline"
|
||||
disabled={isStartDisabled}
|
||||
onClick={() => handleDurationSelect(customDuration * 60 * 1000)}
|
||||
title={
|
||||
isStartDisabled
|
||||
? 'Please select or enter a duration'
|
||||
: `Start ${customDuration}-minute timer`
|
||||
}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
@@ -71,6 +78,7 @@ const InProgressContent = ({ remainingMS, updateTimer, cancelTimer }) => {
|
||||
type="button"
|
||||
className="timer-box__durations__item__btn"
|
||||
onClick={() => updateTimer(addMS)}
|
||||
title={`Add ${addMS / 60000} minutes to timer`}
|
||||
>{`+ ${addMS / 60 / 1000} minutes`}</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -79,6 +87,7 @@ const InProgressContent = ({ remainingMS, updateTimer, cancelTimer }) => {
|
||||
type="button"
|
||||
className="timer-box__btn"
|
||||
onClick={() => cancelTimer()}
|
||||
title="Cancel timer and resume endless playback"
|
||||
>
|
||||
Cancel Timer
|
||||
</button>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
.timer-box {
|
||||
border: 1px solid $primaryColor;
|
||||
background-color: $backgroundColor;
|
||||
color: $primaryColor;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
|
||||
@@ -25,16 +26,20 @@
|
||||
padding: 0.5em 1em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $interactColor;
|
||||
}
|
||||
@include hoverStyle(#{&}) {
|
||||
&:hover {
|
||||
background: $primaryHoverColor;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $activeInteractColor;
|
||||
&:active {
|
||||
background: $primaryActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #ccc;
|
||||
color: $secondaryColor;
|
||||
background: $primaryActiveColor;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +62,20 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
color: $primaryColor;
|
||||
|
||||
// no need for @include hoverStyle since clicking
|
||||
// will hide these
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #ccc;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import ControlButton from '../control-button';
|
||||
|
||||
const IconComponent = ({ pct, onClick }) => {
|
||||
const IconComponent = ({ pct, onClick, title }) => {
|
||||
let icon;
|
||||
if (pct <= 0) {
|
||||
icon = faVolumeMute;
|
||||
@@ -16,12 +16,13 @@ const IconComponent = ({ pct, onClick }) => {
|
||||
} else {
|
||||
icon = faVolumeUp;
|
||||
}
|
||||
return <ControlButton faIcon={icon} onClick={onClick} />;
|
||||
return <ControlButton faIcon={icon} onClick={onClick} title={title} />;
|
||||
};
|
||||
|
||||
IconComponent.propTypes = {
|
||||
pct: propTypes.number.isRequired,
|
||||
onClick: propTypes.func.isRequired,
|
||||
title: propTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IconComponent;
|
||||
|
||||
@@ -8,7 +8,11 @@ const VolumeComponent = ({ volumePct, isMuted, onChange, mute, unmute }) => {
|
||||
const displayPct = isMuted ? 0 : volumePct;
|
||||
return (
|
||||
<div className="volume">
|
||||
<Icon pct={displayPct} onClick={isMuted ? unmute : mute} />
|
||||
<Icon
|
||||
pct={displayPct}
|
||||
onClick={isMuted ? unmute : mute}
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
/>
|
||||
<Slider pct={displayPct} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ const SliderComponent = ({ pct, onChange }) => {
|
||||
onDragStart={preventDefault}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
title="Click or drag to change volume"
|
||||
>
|
||||
<div
|
||||
className="slider__bar"
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
faEllipsisH,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import Popover from 'react-tiny-popover';
|
||||
import IconButton from '@components/shared/icon-button';
|
||||
import FavoriteButton from '@containers/favorite-button.container';
|
||||
import MoreButton from '@components/shared/more-button';
|
||||
import formatPlayTime from './format-play-time';
|
||||
import defaultImage from '@images/default.png';
|
||||
import './piece.scss';
|
||||
@@ -68,7 +71,7 @@ const Piece = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="piece" key={piece.id} onClick={() => onPieceClick(piece)}>
|
||||
<div className="piece" key={piece.id}>
|
||||
<div
|
||||
className="piece__image"
|
||||
onClick={
|
||||
@@ -88,60 +91,14 @@ const Piece = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="piece__btns">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames('piece__btns__btn', 'piece__btns__btn--heart', {
|
||||
'piece__btns__btn--heart--is-filled': isFavorite,
|
||||
})}
|
||||
onClick={
|
||||
isFavorite
|
||||
? () => removeFavorite(piece.id)
|
||||
: () => addFavorite(piece.id)
|
||||
}
|
||||
title={isFavorite ? 'Unfavorite' : 'Favorite'}
|
||||
>
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<FavoriteButton className="piece__btns__btn" pieceId={piece.id} />
|
||||
<IconButton
|
||||
className="piece__btns__btn"
|
||||
faIcon={isSelected && (isPlaying || isLoading) ? faStop : faPlay}
|
||||
onClick={isPlaying ? () => onStopClick() : () => onPlayClick()}
|
||||
title={isPlaying ? 'Stop' : 'Play'}
|
||||
>
|
||||
<FontAwesomeIcon icon={isPlaying && isSelected ? faStop : faPlay} />
|
||||
</button>
|
||||
<Popover
|
||||
isOpen={isMenuOpen}
|
||||
onClickOutside={() => setIsMenuOpen(false)}
|
||||
content={
|
||||
<div className="piece__btns__menu">
|
||||
<button
|
||||
type="button"
|
||||
className="piece__btns__menu__btn"
|
||||
onClick={() => copyLinkToClipboard()}
|
||||
>
|
||||
Copy link to piece
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
align="end"
|
||||
disableReposition="true"
|
||||
padding="-30"
|
||||
transitionDuration="0"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="piece__btns__btn"
|
||||
title="More..."
|
||||
onClick={
|
||||
isMenuOpen
|
||||
? () => setIsMenuOpen(false)
|
||||
: () => setIsMenuOpen(true)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisH} />
|
||||
</button>
|
||||
</Popover>
|
||||
/>
|
||||
<MoreButton className="piece__btns__btn" pieceId={piece.id} />
|
||||
</div>
|
||||
<div className="piece__info">
|
||||
<div className="piece__info__title">
|
||||
|
||||
@@ -117,65 +117,7 @@ $pieceWidth: 12em;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__btn {
|
||||
background: none;
|
||||
color: $primaryColor;
|
||||
font-size: 1.25em;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@include hoverStyle(#{&}) {
|
||||
&:hover {
|
||||
color: $primaryHoverColor;
|
||||
}
|
||||
&:active {
|
||||
color: $primaryActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
&--heart {
|
||||
&--is-filled {
|
||||
color: #e0245e;
|
||||
|
||||
@include hoverStyle(#{&}) {
|
||||
&:hover {
|
||||
color: #b31c4b;
|
||||
}
|
||||
&:active {
|
||||
color: #861538;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
border: 1px solid $primaryColor;
|
||||
|
||||
&__btn {
|
||||
padding: 0.5em;
|
||||
background-color: $backgroundColor;
|
||||
color: $primaryColor;
|
||||
border: none;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
|
||||
// don't need @include hoverStyle since the menu will close on click
|
||||
&:hover {
|
||||
background-color: hsl(0, 0%, 20%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: hsl(0, 0%, 30%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/components/shared/favorite-button.jsx
Normal file
35
src/components/shared/favorite-button.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { faHeart } from '@fortawesome/free-solid-svg-icons';
|
||||
import IconButton from './icon-button';
|
||||
|
||||
const FavoriteButton = ({
|
||||
pieceId,
|
||||
favorites,
|
||||
addFavorite,
|
||||
removeFavorite,
|
||||
className,
|
||||
}) => {
|
||||
const isFavorite = favorites.has(pieceId);
|
||||
return (
|
||||
<IconButton
|
||||
faIcon={faHeart}
|
||||
onClick={
|
||||
isFavorite ? () => removeFavorite(pieceId) : () => addFavorite(pieceId)
|
||||
}
|
||||
title={isFavorite ? 'Unfavorite' : 'Favorite'}
|
||||
isFilled={isFavorite}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FavoriteButton.propTypes = {
|
||||
pieceId: propTypes.string.isRequired,
|
||||
favorites: propTypes.object.isRequired,
|
||||
addFavorite: propTypes.func.isRequired,
|
||||
removeFavorite: propTypes.func.isRequired,
|
||||
className: propTypes.string,
|
||||
};
|
||||
|
||||
export default FavoriteButton;
|
||||
36
src/components/shared/icon-button/icon-button.scss
Normal file
36
src/components/shared/icon-button/icon-button.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
@import '_mixins.scss';
|
||||
@import '_colors.scss';
|
||||
|
||||
.icon-button {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: $primaryColor;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@include hoverStyle(#{&}) {
|
||||
&:hover {
|
||||
color: $primaryHoverColor;
|
||||
}
|
||||
&:active {
|
||||
color: $primaryActiveColor;
|
||||
}
|
||||
}
|
||||
|
||||
&--is-filled {
|
||||
color: #e0245e;
|
||||
|
||||
@include hoverStyle(#{&}) {
|
||||
&:hover {
|
||||
color: #b31c4b;
|
||||
}
|
||||
&:active {
|
||||
color: #861538;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/components/shared/icon-button/index.jsx
Normal file
34
src/components/shared/icon-button/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import './icon-button.scss';
|
||||
|
||||
const IconButton = ({
|
||||
faIcon,
|
||||
onClick,
|
||||
title,
|
||||
className = '',
|
||||
isFilled = false,
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
className={classNames('icon-button', className, {
|
||||
'icon-button--is-filled': isFilled,
|
||||
})}
|
||||
>
|
||||
<FontAwesomeIcon icon={faIcon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
IconButton.propTypes = {
|
||||
faIcon: propTypes.object.isRequired,
|
||||
onClick: propTypes.func.isRequired,
|
||||
title: propTypes.string.isRequired,
|
||||
className: propTypes.string,
|
||||
isFilled: propTypes.bool,
|
||||
};
|
||||
|
||||
export default IconButton;
|
||||
68
src/components/shared/more-button/index.jsx
Normal file
68
src/components/shared/more-button/index.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import Popover from 'react-tiny-popover';
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
|
||||
import IconButton from '@components/shared/icon-button';
|
||||
import './more-button.scss';
|
||||
|
||||
const execCommandCopy = text => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const clipboardApiCopy = text => navigator.clipboard.writeText(text);
|
||||
|
||||
const copyFn = navigator.clipboard ? clipboardApiCopy : execCommandCopy;
|
||||
|
||||
const MoreButton = ({ pieceId, className }) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const copyLinkToClipboard = () => {
|
||||
const link = `${location.origin}/music/${pieceId}`;
|
||||
copyFn(link);
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
isOpen={isMenuOpen}
|
||||
onClickOutside={() => setIsMenuOpen(false)}
|
||||
content={
|
||||
<div className="more-btn__menu">
|
||||
<button
|
||||
type="button"
|
||||
className="more-btn__menu__btn"
|
||||
onClick={() => copyLinkToClipboard()}
|
||||
>
|
||||
Copy link to piece
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
align="end"
|
||||
disableReposition="true"
|
||||
padding="-25"
|
||||
transitionDuration="0"
|
||||
>
|
||||
<IconButton
|
||||
faIcon={faEllipsisH}
|
||||
className={className}
|
||||
title="More..."
|
||||
onClick={
|
||||
isMenuOpen ? () => setIsMenuOpen(false) : () => setIsMenuOpen(true)
|
||||
}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
MoreButton.propTypes = {
|
||||
pieceId: propTypes.string.isRequired,
|
||||
className: propTypes.string,
|
||||
};
|
||||
|
||||
export default MoreButton;
|
||||
29
src/components/shared/more-button/more-button.scss
Normal file
29
src/components/shared/more-button/more-button.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
@import '_colors.scss';
|
||||
|
||||
.more-btn {
|
||||
&__menu {
|
||||
border: 1px solid $primaryColor;
|
||||
|
||||
&__btn {
|
||||
padding: 0.5em;
|
||||
background-color: $backgroundColor;
|
||||
color: $primaryColor;
|
||||
border: none;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
|
||||
// don't need @include hoverStyle since the menu will close on click
|
||||
&:hover {
|
||||
background-color: hsl(0, 0%, 20%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: hsl(0, 0%, 30%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import CurrentlyPlayingComponent from '../components/app/controls/currently-playing';
|
||||
|
||||
const mapStateToProps = ({ selectedPieceId }) => ({
|
||||
const mapStateToProps = ({ selectedPieceId, isPlaying }) => ({
|
||||
selectedPieceId,
|
||||
isPlaying,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(CurrentlyPlayingComponent);
|
||||
|
||||
11
src/containers/favorite-button.container.js
Normal file
11
src/containers/favorite-button.container.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import FavoriteButton from '@components/shared/favorite-button';
|
||||
import addFavorite from '@store/actions/creators/add-favorite.creator';
|
||||
import removeFavorite from '@store/actions/creators/remove-favorite.creator';
|
||||
|
||||
const mapStateToProps = ({ favorites }) => ({ favorites });
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{ addFavorite, removeFavorite }
|
||||
)(FavoriteButton);
|
||||
@@ -1,6 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import VisualizerComponent from '../components/app/controls/currently-playing/visualizer';
|
||||
|
||||
const mapStateToProps = ({ isPlaying }) => ({ isPlaying });
|
||||
|
||||
export default connect(mapStateToProps)(VisualizerComponent);
|
||||
@@ -15,7 +15,7 @@ const log = msg => console.log(msg);
|
||||
const makeConfig = alias => ({
|
||||
mode: 'development',
|
||||
devtool: 'sourcemap',
|
||||
entry: ['babel-polyfill', './src'],
|
||||
entry: ['@babel/polyfill', './src'],
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: '[name].[hash].js',
|
||||
|
||||
Reference in New Issue
Block a user