diff --git a/src/components/RadioElement.js b/src/components/RadioElement.js index 8bb0f2c..1c18342 100644 --- a/src/components/RadioElement.js +++ b/src/components/RadioElement.js @@ -1,38 +1,39 @@ -import React, { Component } from "react"; +import React from "react"; import PropTypes from "prop-types"; import styled from "styled-components"; -import classNames from "classnames"; import RoundNumber from "./RoundNumber"; +import { useBreakpoint } from "../helpers/hooks"; +import breakpoint from "../helpers/breakpoint"; + import consts from "../consts.js"; import music from "../img/type/1music.png"; import speech from "../img/type/2speech.png"; import ads from "../img/type/3ads.png"; -const TRANSITION = "0.2s ease-out"; - const Shadow = styled.div` opacity: 0.5; width: 100%; height: 100%; border-radius: 50%; - box-shadow: 0px 0px 15px 0px rgb(0, 0, 0); + border: 1px solid black; - &.playing { - opacity: 0.2; + ${props => + props.playing && breakpoint.min.l` + opacity: 0.2; box-shadow: 0px 0px 60px 20px rgb(0, 0, 0); animation-name: shadow; animation-duration: 3s; animation-timing-function: linear; animation-iteration-count: infinite; - } + `} - &.condensed { - box-shadow: unset; - border: 1px solid black; - } + ${breakpoint.min.l` + box-shadow: 0px 0px 15px 0px rgb(0, 0, 0); + border: none; + `} @keyframes shadow { 0% { @@ -55,67 +56,63 @@ const RadioName = styled.span` const RadioStatus = styled.div` position: absolute; - transition: ${TRANSITION}; + transition: 0.2s ease-out; border-radius: 50%; `; const RadioStatusLogo = styled.img` - filter: invert(100%); width: 80%; - margin: 10%; height: 80%; + margin: 10%; + filter: invert(100%); `; -class RadioElement extends Component { - constructor(props) { - super(props); - this.onClick = this.props.onClick.bind(this); - } +function RadioElement(props) { + const { radio, style, onClick, position } = props; - render() { - let radio = this.props.radio; - return ( -
- {radio.name} -
- -
- {!this.props.condensed && ( - - {radio.name} - - )} - {this.props.position && ( - - )} - - - - - - - - - + const isDesktop = useBreakpoint("l"); + + return ( + <> + {radio.name} +
+
- ); - } + + {isDesktop && ( + + {radio.name} + + )} + + {position && ( + + )} + + + + + + + + + + + + ); } +RadioElement.defaultProps = { + position: PropTypes.object, +}; + RadioElement.propTypes = { radio: PropTypes.object.isRequired, onClick: PropTypes.func.isRequired, - condensed: PropTypes.bool.isRequired, position: PropTypes.object, style: PropTypes.object.isRequired }; diff --git a/src/components/RadiosCarousel.js b/src/components/RadiosCarousel.js index a9bb872..941b6e4 100644 --- a/src/components/RadiosCarousel.js +++ b/src/components/RadiosCarousel.js @@ -1,9 +1,10 @@ -import React, { Component } from "react"; +import React, { useEffect, useRef } from "react"; import PropTypes from "prop-types"; import RadioElement from "./RadioElement"; import Controls from "./Controls/Controls"; +import { throttle } from "../helpers/functions"; import consts from "../consts.js"; const LOGO_SIZE = 200; @@ -19,233 +20,226 @@ const styleDivRadios = { height: "100%" }; -class RadiosCarousel extends Component { - componentDidMount() { - document.addEventListener("keydown", this.handleNav); +function RadiosCarousel(props) { + const { bsw, settings, condensed } = props; - let self = this; - this.lastScroll = new Date(); - this.radiolistRef.current.addEventListener("wheel", function(event) { - let now = new Date(); // avoid too fast scrolling - if (+now - self.lastScroll < 300) return; - self.lastScroll = now; - self.props.bsw.manualTuning(event.deltaY > 0 ? 1 : -1); + const carouselRef = useRef(null); + + let radios = settings.radios; + + const handleArrowKeys = evt => { + if (evt.key === "ArrowDown") { + bsw.manualTuning(1); + } else if (evt.key === "ArrowUp") { + bsw.manualTuning(-1); + } + }; + + const handleRadioClick = i => { + let targetVolume = bsw.getFilteredVolume(settings.radios[i].getStatus()); + if (targetVolume === consts.VOLUME_NORMAL || settings.config.actionType === consts.ACTION_MUTE) { + bsw.togglePlay(settings.radios[i].name); + } + }; + + const handleScroll = throttle(evt => { + bsw.manualTuning(evt.deltaY > 0 ? 1 : -1); + }, 300); + + + useEffect(() => { + // TODO: This code have to move in a service or something + for (let i = 0; i < radios.length; i++) { + bsw.getRadio(radios[i].country, radios[i].name, result => { + result.logo = result.favicon; + settings.addRadio(result); + }); + } + }, []); // No deps, original code was fired once + + useEffect(() => { + const carouselRefCopy = carouselRef.current; + document.addEventListener("keydown", handleArrowKeys); + carouselRefCopy.addEventListener("wheel", handleScroll); + + return () => { + document.removeEventListener("keydown", handleArrowKeys); + carouselRefCopy.removeEventListener("wheel", handleScroll); + }; + }, []); + + + let indexRadio = bsw.getActiveIndex(); + let noFilter = settings.config.filterType === consts.FILTER_OFF; + let isPodium = settings.config.actionType === consts.ACTION_PODIUM; + + let styleLogo = [], + styleOverlay = [], + styleName = [], + styleStatus = [], + positionData = []; + + let translate = []; + for (let i = 0; i < radios.length; i++) { + translate.push(i); + } + + for (let i = 0; i < radios.length; i++) { + let delta = translate[i] - indexRadio; + let scalef, angleRotate, logoL, logoT, logoS, zIndex, shadow, fontS, statusR, statusS, controlShift; + + if (isNaN(delta)) { + delta = i - (radios.length - 1) / 2; + } + + let acceptableContent = 1; + + let targetVolume = bsw.getFilteredVolume(settings.radios[translate[i]].getStatus()); + if (targetVolume === consts.VOLUME_MUTED && !noFilter) { + acceptableContent = 0; + } + + let scalef_base = 1 - 0.5 * condensed; + + if (isNaN(indexRadio)) { + logoT = 5 + ((i + 1.0) / (radios.length + 1)) * 95; + scalef = scalef_base; + logoL = 50; + zIndex = i * 100; + } else if (settings.config.actionType === consts.ACTION_PODIUM) { + logoT = 5 + ((i + 1.0) / (radios.length + 1)) * 95; + logoL = 50; + scalef = scalef_base * (1 + 0.5 * (translate[i] === indexRadio)); + zIndex = (radios.length - Math.abs(delta)) * 100; + } else { + logoT = 5 + ((i + 1.0 + 0.5 * (i === indexRadio) + 1 * (i > indexRadio)) / (radios.length + 2)) * 95; + logoL = 50 + Math.abs(i - indexRadio) * 3; + scalef = scalef_base * (1 + 0.5 * (translate[i] === indexRadio)); + zIndex = (radios.length - Math.abs(delta)) * 100; + } + + shadow = noFilter ? 0 : 0.8 * (1 - acceptableContent); + fontS = Math.round(18 + 6 * acceptableContent) * scalef; + + angleRotate = 0; + statusR = LOGO_SIZE * scalef; // radius for position of status dots + logoS = LOGO_SIZE * scalef; // diameter of radio logo + shadow + statusS = STATUS_SIZE * (condensed ? 1 : scalef); // diameter of status dots + controlShift = Math.round((logoT * CONTROLS_HEIGHT) / 100); + + let styleLogoBase = { + left: "" + logoL + "%", + top: "calc(" + logoT + "% - " + controlShift + "px)", + borderRadius: "50%", + transition: TRANSITION, + position: "absolute" + }; + + styleLogo.push( + Object.assign({}, styleLogoBase, { + width: "" + logoS + "px", + height: "" + logoS + "px", + marginLeft: "" + -logoS / 2 + "px", + marginTop: "" + -logoS / 2 + "px", + background: "white", + zIndex: "" + zIndex, + transform: "rotate(" + (-angleRotate * 180) / Math.PI + "deg)" + }) + ); + + styleOverlay.push( + Object.assign({}, styleLogoBase, { + width: "" + logoS + "px", + height: "" + logoS + "px", + marginLeft: "" + -logoS / 2 + "px", + marginTop: "" + -logoS / 2 + "px", + background: "rgba(187,187,187," + shadow + ")", + zIndex: "" + (zIndex + 1), + cursor: + acceptableContent || settings.config.actionType === consts.ACTION_MUTE ? "pointer" : "not-allowed" + }) + ); + + styleName.push({ + left: "calc(" + logoL + "% + " + (logoS * (0.5 + 1 / 8) + 30) * Math.cos(-angleRotate) + "px)", + top: "calc(" + logoT + "% + " + ((logoS * (0.5 + 1 / 8) + 30) * Math.sin(-angleRotate) - controlShift) + "px)", + fontSize: "" + fontS + "px", + fontWeight: "bold", + color: "rgba(0,0,0," + (1 - shadow) + ")", + marginTop: "-0.5em", + zIndex: "" + zIndex, + transformOrigin: "left", + transform: "rotate(" + (-angleRotate * 180) / Math.PI + "deg)", + transition: TRANSITION, + cursor: acceptableContent ? "pointer" : "not-allowed" }); - let radios = this.props.settings.radios; - for (let i = 0; i < radios.length; i++) { - this.props.bsw.getRadio(radios[i].country, radios[i].name, result => { - result.logo = result.favicon; - this.props.settings.addRadio(result); - }); - } - } - - componentWillUnmount() { - document.removeEventListener("keydown", this.handleNav); - } - - constructor(props) { - super(props); - this.handleRadioClick = this.handleRadioClick.bind(this); - this.handleNav = this.handleNav.bind(this); - this.radiolistRef = React.createRef(); - } - - handleRadioClick(i) { - let targetVolume = this.props.bsw.getFilteredVolume(this.props.settings.radios[i].getStatus()); - if (targetVolume === consts.VOLUME_NORMAL || this.props.settings.config.actionType === consts.ACTION_MUTE) { - this.props.bsw.togglePlay(this.props.settings.radios[i].name); - } - } - - handleNav(evt) { - let delta = 0; - if (evt.key === "ArrowDown") { - delta = 1; - } else if (evt.key === "ArrowUp") { - delta = -1; - } - this.props.bsw.manualTuning(delta); - } - - render() { - let radios = this.props.settings.radios; - let self = this; - let indexRadio = this.props.bsw.getActiveIndex(); - let noFilter = this.props.settings.config.filterType === consts.FILTER_OFF; - let isPodium = this.props.settings.config.actionType === consts.ACTION_PODIUM; - let styleLogo = [], - styleOverlay = [], - styleName = [], - styleStatus = [], - positionData = []; - - let translate = []; - for (let i = 0; i < radios.length; i++) { - translate.push(i); - } - - for (let i = 0; i < radios.length; i++) { - let delta = translate[i] - indexRadio; - let scalef, angleRotate, logoL, logoT, logoS, zIndex, shadow, fontS, statusR, statusS, controlShift; - if (isNaN(delta)) delta = i - (radios.length - 1) / 2; - - let acceptableContent = 1; - - let targetVolume = this.props.bsw.getFilteredVolume(this.props.settings.radios[translate[i]].getStatus()); - if (targetVolume === consts.VOLUME_MUTED && !noFilter) { - acceptableContent = 0; + styleStatus.push(function(status, condensed) { + let statusAngle = 0, + statusBgColor = consts.getStatusColor(status); + switch (status) { + case consts.STATUS_AD: + statusAngle = -STATUS_ANGLE * (1 + 0.5 * condensed); + break; + case consts.STATUS_MUSIC: + statusAngle = +STATUS_ANGLE * (1 + 0.5 * condensed); + break; + default: } - - let scalef_base = 1 - 0.5 * this.props.condensed; - - if (isNaN(indexRadio)) { - logoT = 5 + ((i + 1.0) / (radios.length + 1)) * 95; - scalef = scalef_base; - logoL = 50; - zIndex = i * 100; - } else if (this.props.settings.config.actionType === consts.ACTION_PODIUM) { - //angle = 0; - logoT = 5 + ((i + 1.0) / (radios.length + 1)) * 95; - logoL = 50; - scalef = scalef_base * (1 + 0.5 * (translate[i] === indexRadio)); - zIndex = (radios.length - Math.abs(delta)) * 100; - } else { - logoT = 5 + ((i + 1.0 + 0.5 * (i === indexRadio) + 1 * (i > indexRadio)) / (radios.length + 2)) * 95; - logoL = 50 + Math.abs(i - indexRadio) * 3; - scalef = scalef_base * (1 + 0.5 * (translate[i] === indexRadio)); - zIndex = (radios.length - Math.abs(delta)) * 100; - } - - shadow = noFilter ? 0 : 0.8 * (1 - acceptableContent); - fontS = Math.round(18 + 6 * acceptableContent) * scalef; - - angleRotate = 0; - statusR = LOGO_SIZE * scalef; // radius for position of status dots - logoS = LOGO_SIZE * scalef; // diameter of radio logo + shadow - statusS = STATUS_SIZE * (this.props.condensed ? 1 : scalef); // diameter of status dots - controlShift = Math.round((logoT * CONTROLS_HEIGHT) / 100); - - let styleLogoBase = { - left: "" + logoL + "%", - top: "calc(" + logoT + "% - " + controlShift + "px)", - borderRadius: "50%", - transition: TRANSITION, - position: "absolute" - }; - - styleLogo.push( - Object.assign({}, styleLogoBase, { - width: "" + logoS + "px", - height: "" + logoS + "px", - marginLeft: "" + -logoS / 2 + "px", - marginTop: "" + -logoS / 2 + "px", - background: "white", - zIndex: "" + zIndex, - transform: "rotate(" + (-angleRotate * 180) / Math.PI + "deg)" - }) - ); - - styleOverlay.push( - Object.assign({}, styleLogoBase, { - width: "" + logoS + "px", - height: "" + logoS + "px", - marginLeft: "" + -logoS / 2 + "px", - marginTop: "" + -logoS / 2 + "px", - background: "rgba(187,187,187," + shadow + ")", - zIndex: "" + (zIndex + 1), - cursor: - acceptableContent || this.props.settings.config.actionType === consts.ACTION_MUTE - ? "pointer" - : "not-allowed" - }) - ); - - styleName.push({ - left: "calc(" + logoL + "% + " + (logoS * (0.5 + 1 / 8) + 30) * Math.cos(-angleRotate) + "px)", - top: "calc(" + logoT + "% + " + ((logoS * (0.5 + 1 / 8) + 30) * Math.sin(-angleRotate) - controlShift) + "px)", - fontSize: "" + fontS + "px", - fontWeight: "bold", - color: "rgba(0,0,0," + (1 - shadow) + ")", - marginTop: "-0.5em", - zIndex: "" + zIndex, - transformOrigin: "left", + return { + left: "calc(" + logoL + "% - " + (statusR / 2) * 1.1 * Math.cos(statusAngle + angleRotate) + "px)", + top: + "calc(" + logoT + "% + " + ((statusR / 2) * 1.1 * Math.sin(statusAngle + angleRotate) - controlShift) + "px)", + width: statusS + "px", + height: statusS + "px", transform: "rotate(" + (-angleRotate * 180) / Math.PI + "deg)", - transition: TRANSITION, - cursor: acceptableContent ? "pointer" : "not-allowed" - }); - - styleStatus.push(function(status, condensed) { - let statusAngle = 0, - statusBgColor = consts.getStatusColor(status); - switch (status) { - case consts.STATUS_AD: - statusAngle = -STATUS_ANGLE * (1 + 0.5 * condensed); - break; - case consts.STATUS_MUSIC: - statusAngle = +STATUS_ANGLE * (1 + 0.5 * condensed); - break; - default: - } - return { - left: "calc(" + logoL + "% - " + (statusR / 2) * 1.1 * Math.cos(statusAngle + angleRotate) + "px)", - top: - "calc(" + - logoT + - "% + " + - ((statusR / 2) * 1.1 * Math.sin(statusAngle + angleRotate) - controlShift) + - "px)", - width: statusS + "px", - height: statusS + "px", - transform: "rotate(" + (-angleRotate * 180) / Math.PI + "deg)", - zIndex: "" + (zIndex + 1), - marginLeft: -statusS / 2 + "px", - marginTop: -statusS / 2 + "px", - transition: TRANSITION, - background: radios[i].getStatus() === status ? statusBgColor : "#bbbbbb" - }; - }); - - positionData.push({ - left: "calc(" + logoL + "% + " + logoS * 0.3 + "px)", - top: "calc(" + logoT + "% - " + (logoS * 0.5 - controlShift) + "px)", - position: "absolute", zIndex: "" + (zIndex + 1), - fontSize: "" + fontS + "px", - fontWeight: "bold", - opacity: isPodium ? "1" : "0", - transition: TRANSITION - }); - } + marginLeft: -statusS / 2 + "px", + marginTop: -statusS / 2 + "px", + transition: TRANSITION, + background: radios[i].getStatus() === status ? statusBgColor : "#bbbbbb" + }; + }); - return ( -
- - {radios.map(function(radio, i) { - let ti = translate.indexOf(i); - return ( - self.handleRadioClick(ti)} - /> - ); - })} -
- ); + positionData.push({ + left: "calc(" + logoL + "% + " + logoS * 0.3 + "px)", + top: "calc(" + logoT + "% - " + (logoS * 0.5 - controlShift) + "px)", + position: "absolute", + zIndex: "" + (zIndex + 1), + fontSize: "" + fontS + "px", + fontWeight: "bold", + opacity: isPodium ? "1" : "0", + transition: TRANSITION + }); } + + return ( +
+ + + {radios.map(function(radio, i) { + let ti = translate.indexOf(i); + return ( + handleRadioClick(ti)} + /> + ); + })} +
+ ); } RadiosCarousel.propTypes = {