diff --git a/js/components/ClipPaths.tsx b/js/components/ClipPaths.tsx new file mode 100644 index 00000000..beb823a5 --- /dev/null +++ b/js/components/ClipPaths.tsx @@ -0,0 +1,34 @@ +import React, { useMemo, useLayoutEffect } from "react"; +import { createPortal } from "react-dom"; + +type Props = { + children: { [id: string]: string[] }; +}; + +// this.props.children should be an object containing arrays of strings. The +// keys are ids, and the arrays are arrays of polygon point strings +export default function ClipPaths({ children }: Props) { + const paths = useMemo(() => { + return document.createElement("div"); + }, []); + + useLayoutEffect(() => { + document.body.appendChild(paths); + return () => paths.remove(); + }, [paths]); + + return createPortal( + , + paths + ); +} diff --git a/js/components/Css.tsx b/js/components/Css.tsx new file mode 100644 index 00000000..97672e56 --- /dev/null +++ b/js/components/Css.tsx @@ -0,0 +1,23 @@ +import { createPortal } from "react-dom"; +import { useMemo, useLayoutEffect } from "react"; + +type Props = { + children: string; + id: string; +}; + +export default function Css({ children, id }: Props) { + const style = useMemo(() => { + const s = document.createElement("style"); + s.type = "text/css"; + s.id = id; + return s; + }, [id]); + + useLayoutEffect(() => { + document.head.appendChild(style); + return () => style.remove(); + }, [style]); + + return createPortal(children, style); +} diff --git a/js/components/Skin.js b/js/components/Skin.js deleted file mode 100644 index eb3d8133..00000000 --- a/js/components/Skin.js +++ /dev/null @@ -1,171 +0,0 @@ -// Dynamically set the css background images for all the sprites -import React from "react"; -import { createPortal } from "react-dom"; -import { connect } from "react-redux"; - -import { LETTERS } from "../constants"; -import { imageSelectors, cursorSelectors } from "../skinSelectors"; - -const CSS_PREFIX = "#webamp"; - -const mapRegionNamesToIds = { - normal: "mainWindowClipPath", - windowshade: "shadeMainWindowClipPath", - equalizer: "equalizerWindowClipPath", - equalizerws: "shadeEqualizerWindowClipPath", -}; - -const mapRegionNamesToMatcher = { - normal: "#main-window:not(.shade)", - windowshade: "#main-window.shade", - equalizer: "#equalizer-window:not(.shade)", - equalizerws: "#equalizer-window.shade", -}; - -const numExIsUsed = skinImages => !!skinImages.DIGIT_0_EX; - -function Css({ children }) { - const style = React.useMemo(() => { - const s = document.createElement("style"); - s.type = "text/css"; - s.id = "webamp-skin"; - return s; - }, []); - - React.useLayoutEffect(() => { - document.head.appendChild(style); - return () => style.remove(); - }, [style]); - - return createPortal(children, style); -} -// this.props.children should be an object containing arrays of strings. The -// keys are ids, and the arrays are arrays of polygon point strings -function ClipPaths({ children }) { - const paths = React.useMemo(() => { - return document.createElement("div"); - }, []); - - React.useLayoutEffect(() => { - document.body.appendChild(paths); - return () => paths.remove(); - }, [paths]); - - return createPortal( - , - paths - ); -} - -const FALLBACKS = { - MAIN_BALANCE_BACKGROUND: "MAIN_VOLUME_BACKGROUND", - MAIN_BALANCE_THUMB: "MAIN_VOLUME_THUMB", - MAIN_BALANCE_THUMB_ACTIVE: "MAIN_VOLUME_THUMB_SELECTED", -}; - -function cssRulesFromProps(props) { - const { skinImages, skinCursors, skinGenLetterWidths } = props; - if (!skinImages || !skinCursors) { - return null; - } - const cssRules = []; - Object.keys(imageSelectors).forEach(imageName => { - const imageUrl = skinImages[imageName] || skinImages[FALLBACKS[imageName]]; - if (imageUrl) { - imageSelectors[imageName].forEach(selector => { - cssRules.push( - `${CSS_PREFIX} ${selector} {background-image: url(${imageUrl})}` - ); - }); - } - }); - - if (skinGenLetterWidths != null) { - LETTERS.forEach(letter => { - const width = skinGenLetterWidths[`GEN_TEXT_${letter}`]; - const selectedWidth = skinGenLetterWidths[`GEN_TEXT_SELECTED_${letter}`]; - cssRules.push( - `${CSS_PREFIX} .gen-text-${letter.toLowerCase()} {width: ${width}px;}` - ); - cssRules.push( - `${CSS_PREFIX} .selected .gen-text-${letter.toLowerCase()} {width: ${selectedWidth}px;}` - ); - }); - } - Object.keys(cursorSelectors).forEach(cursorName => { - const cursorUrl = skinCursors[cursorName]; - if (cursorUrl) { - cursorSelectors[cursorName].forEach(selector => { - cssRules.push( - `${ - // TODO: Fix this hack - // Maybe our CSS name spacing should be based on some other class/id - // than the one we use for defining the main div. - // That way it could be shared by both the player and the context menu. - selector.startsWith("#webamp-context-menu") ? "" : CSS_PREFIX - } ${selector} {cursor: url(${cursorUrl}), auto}` - ); - }); - } - }); - - if (numExIsUsed(skinImages)) { - // This alternate number file requires that the minus sign be - // formatted differently. - cssRules.push( - `${CSS_PREFIX} .status #time #minus-sign { top: 0px; left: -1px; width: 9px; height: 13px; }` - ); - } - - for (const [regionName, polygons] of Object.entries(props.skinRegion)) { - if (polygons) { - const matcher = mapRegionNamesToMatcher[regionName]; - const id = mapRegionNamesToIds[regionName]; - cssRules.push(`${CSS_PREFIX} ${matcher} { clip-path: url(#${id}); }`); - } - } - - return cssRules; -} - -function clipPathsFromProps(props) { - const clipPaths = {}; - for (const [regionName, polygons] of Object.entries(props.skinRegion)) { - if (polygons) { - const id = mapRegionNamesToIds[regionName]; - clipPaths[id] = polygons; - } - } - return clipPaths; -} - -const Skin = props => { - const cssRules = cssRulesFromProps(props); - if (cssRules == null) { - return null; - } - const clipPaths = clipPathsFromProps(props); - return ( -