mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 11:04:00 +00:00
Make the Skin component use hooks and type it
This also abstracts some generic utility components
This commit is contained in:
parent
af4990d5af
commit
7bd8c702ac
5 changed files with 209 additions and 171 deletions
34
js/components/ClipPaths.tsx
Normal file
34
js/components/ClipPaths.tsx
Normal file
|
|
@ -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(
|
||||
<svg height={0} width={0}>
|
||||
<defs>
|
||||
{Object.keys(children).map(id => (
|
||||
<clipPath id={id} key={id}>
|
||||
{children[id].map((points, i) => (
|
||||
<polygon points={points} key={i} />
|
||||
))}
|
||||
</clipPath>
|
||||
))}
|
||||
</defs>
|
||||
</svg>,
|
||||
paths
|
||||
);
|
||||
}
|
||||
23
js/components/Css.tsx
Normal file
23
js/components/Css.tsx
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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(
|
||||
<svg height={0} width={0}>
|
||||
<defs>
|
||||
{Object.keys(children).map(id => (
|
||||
<clipPath id={id} key={id}>
|
||||
{children[id].map((points, i) => (
|
||||
<polygon points={points} key={i} />
|
||||
))}
|
||||
</clipPath>
|
||||
))}
|
||||
</defs>
|
||||
</svg>,
|
||||
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 (
|
||||
<div>
|
||||
<Css>{cssRules.join("\n")}</Css>
|
||||
<ClipPaths>{clipPaths}</ClipPaths>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(state => ({
|
||||
skinImages: state.display.skinImages,
|
||||
skinCursors: state.display.skinCursors,
|
||||
skinRegion: state.display.skinRegion,
|
||||
skinGenLetterWidths: state.display.skinGenLetterWidths,
|
||||
}))(Skin);
|
||||
132
js/components/Skin.tsx
Normal file
132
js/components/Skin.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// Dynamically set the css background images for all the sprites
|
||||
import React from "react";
|
||||
|
||||
import { LETTERS } from "../constants";
|
||||
import { imageSelectors, cursorSelectors } from "../skinSelectors";
|
||||
import { useTypedSelector } from "../hooks";
|
||||
import * as Selectors from "../selectors";
|
||||
import { SkinImages } from "../types";
|
||||
import { createSelector } from "reselect";
|
||||
import Css from "./Css";
|
||||
import ClipPaths from "./ClipPaths";
|
||||
|
||||
const CSS_PREFIX = "#webamp";
|
||||
|
||||
const mapRegionNamesToIds: { [key: string]: string } = {
|
||||
normal: "mainWindowClipPath",
|
||||
windowshade: "shadeMainWindowClipPath",
|
||||
equalizer: "equalizerWindowClipPath",
|
||||
equalizerws: "shadeEqualizerWindowClipPath",
|
||||
};
|
||||
|
||||
const mapRegionNamesToMatcher: { [key: string]: string } = {
|
||||
normal: "#main-window:not(.shade)",
|
||||
windowshade: "#main-window.shade",
|
||||
equalizer: "#equalizer-window:not(.shade)",
|
||||
equalizerws: "#equalizer-window.shade",
|
||||
};
|
||||
|
||||
const numExIsUsed = (skinImages: SkinImages) => Boolean(skinImages.DIGIT_0_EX);
|
||||
|
||||
const FALLBACKS: { [key: string]: string } = {
|
||||
MAIN_BALANCE_BACKGROUND: "MAIN_VOLUME_BACKGROUND",
|
||||
MAIN_BALANCE_THUMB: "MAIN_VOLUME_THUMB",
|
||||
MAIN_BALANCE_THUMB_ACTIVE: "MAIN_VOLUME_THUMB_SELECTED",
|
||||
};
|
||||
|
||||
const getCssRules = createSelector(
|
||||
Selectors.getSkinImages,
|
||||
Selectors.getSkinCursors,
|
||||
Selectors.getSkinLetterWidths,
|
||||
Selectors.getSkinRegion,
|
||||
(skinImages, skinCursors, skinGenLetterWidths, skinRegion): string | null => {
|
||||
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(skinRegion)) {
|
||||
if (polygons) {
|
||||
const matcher = mapRegionNamesToMatcher[regionName];
|
||||
const id = mapRegionNamesToIds[regionName];
|
||||
cssRules.push(`${CSS_PREFIX} ${matcher} { clip-path: url(#${id}); }`);
|
||||
}
|
||||
}
|
||||
|
||||
return cssRules.join("\n");
|
||||
}
|
||||
);
|
||||
|
||||
const getClipPaths = createSelector(Selectors.getSkinRegion, skinRegion => {
|
||||
const clipPaths: { [id: string]: string[] } = {};
|
||||
for (const [regionName, polygons] of Object.entries(skinRegion)) {
|
||||
if (polygons) {
|
||||
const id = mapRegionNamesToIds[regionName];
|
||||
clipPaths[id] = polygons;
|
||||
}
|
||||
}
|
||||
return clipPaths;
|
||||
});
|
||||
|
||||
export default function Skin() {
|
||||
const cssRules = useTypedSelector(getCssRules);
|
||||
const clipPaths = useTypedSelector(getClipPaths);
|
||||
if (cssRules == null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Css id="webamp-skin">{cssRules}</Css>
|
||||
<ClipPaths>{clipPaths}</ClipPaths>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,10 @@ import {
|
|||
TransitionType,
|
||||
MediaStatus,
|
||||
TimeMode,
|
||||
SkinImages,
|
||||
Cursors,
|
||||
SkinRegion,
|
||||
GenLetterWidths,
|
||||
} from "./types";
|
||||
import { createSelector, defaultMemoize } from "reselect";
|
||||
import * as Utils from "./utils";
|
||||
|
|
@ -677,6 +681,22 @@ export function getRandomizePresets(state: AppState): boolean {
|
|||
return state.milkdrop.randomize;
|
||||
}
|
||||
|
||||
export function getSkinImages(state: AppState): SkinImages {
|
||||
return state.display.skinImages;
|
||||
}
|
||||
|
||||
export function getSkinCursors(state: AppState): Cursors | null {
|
||||
return state.display.skinCursors;
|
||||
}
|
||||
|
||||
export function getSkinRegion(state: AppState): SkinRegion {
|
||||
return state.display.skinRegion;
|
||||
}
|
||||
|
||||
export function getSkinLetterWidths(state: AppState): GenLetterWidths | null {
|
||||
return state.display.skinGenLetterWidths;
|
||||
}
|
||||
|
||||
export function getPreampLineUrl(state: AppState): string | null {
|
||||
return state.display.skinImages.EQ_PREAMP_LINE;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue