This commit is contained in:
Jordan Eldredge 2018-11-25 21:40:07 -08:00
parent e0b7a32b9d
commit b9cacadcee
14 changed files with 377 additions and 674 deletions

View file

@ -8,9 +8,11 @@
"image-average-color": "^1.0.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.1.1",
"react-scripts": "1.1.1",
"redux": "^4.0.1",
"shell-escape": "^0.2.0",
"webamp": "^1.0.0"
"webamp": "^1.3.0-beta.0"
},
"scripts": {
"start": "react-scripts start",

BIN
public/llama.mp3 Normal file

Binary file not shown.

View file

@ -1,3 +1,11 @@
body {
margin: 0;
}
body.overlay-open {
overflow: hidden;
}
.screenshot {
height: 100%;
opacity: 0;
@ -10,27 +18,8 @@
-webkit-backface-visibility: hidden;
}
.skin:hover {
/*
z-index: 10;
box-shadow: 0px 0px 15px 5px rgba(46, 46, 139, 0.75);
*/
}
.priority {
position: absolute;
background: white;
}
.overlay {
/* recommendation:
don't focus on the number "1000" here, but rather,
you should have a documented system for z-index and
follow that system. This number should be pretty
high on the scale in that system.
*/
z-index: 1000;
position: fixed;
top: 0;
left: 0;
@ -38,3 +27,14 @@
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
}
#close-modal {
color: white;
position: fixed;
top: 10px;
right: 10px;
padding: 0;
font-size: 50px;
line-height: 50px;
text-decoration: none;
}

View file

@ -1,10 +1,12 @@
import React from "react";
import skins from "./skins.json";
import "./App.css";
import { Overlay } from "./Overlay";
import { connect } from "react-redux";
import Overlay from "./Overlay";
import Skin from "./Skin";
import FocusedSkin from "./FocusedSkin";
import * as Utils from "./utils";
import * as Selectors from "./redux/selectors";
import { SKIN_WIDTH, SKIN_HEIGHT, SKIN_RATIO } from "./constants";
const hashes = Object.keys(skins);
@ -24,7 +26,6 @@ class App extends React.Component {
...Utils.getWindowSize(),
scrollTop: getScrollTop(),
scrollDirection: null,
selectedSkinHash: null,
selectedSkinPosition: null
};
this._handleScroll();
@ -34,7 +35,7 @@ class App extends React.Component {
}
_handleSelectSkin(hash, position) {
this.setState({ selectedSkinHash: hash, selectedSkinPosition: position });
this.props.setSelectedSkin(hash, position);
}
_handleResize() {
@ -135,15 +136,9 @@ class App extends React.Component {
>
{skinElements}
</div>
{this.state.selectedSkinHash && (
{this.props.selectedSkinHash == null || (
<Overlay>
<FocusedSkin
hash={this.state.selectedSkinHash}
initialHeight={rowHeight}
initialWidth={columnWidth}
initialTop={this.state.selectedSkinPosition.top}
initialLeft={this.state.selectedSkinPosition.left}
/>
<FocusedSkin initialHeight={rowHeight} initialWidth={columnWidth} />
</Overlay>
)}
</div>
@ -151,4 +146,17 @@ class App extends React.Component {
}
}
export default App;
const mapStateToProps = state => ({
selectedSkinHash: Selectors.getSelectedSkinHash(state)
});
const mapDispatchToProps = dispatch => ({
// TODO: Extract to action creator
setSelectedSkin(hash, position) {
dispatch({ type: "SELECT_SKIN", hash, position });
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

View file

@ -1,30 +1,42 @@
import React from "react";
import { connect } from "react-redux";
import WebampComponent from "./WebampComponent";
import * as Utils from "./utils";
import * as Selectors from "./redux/selectors";
import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from "./constants";
export default class FocusedSkin extends React.Component {
class FocusedSkin extends React.Component {
constructor(props) {
super(props);
// TODO: Handle the case were we come from a permalink
this.state = {
top: this.props.initialTop,
left: this.props.initialLeft,
width: this.props.initialWidth,
height: this.props.initialHeight
};
if (this.props.initialPosition == null) {
this.state = this._getCenteredState();
} else {
this.state = {
top: this.props.initialPosition.top,
left: this.props.initialPosition.left,
width: this.props.initialWidth,
height: this.props.initialHeight
};
}
}
componentDidMount() {
setTimeout(() => {
// TODO: Observe DOM and recenter
const { windowWidth, windowHeight } = Utils.getWindowSize();
this.setState({
top: (windowHeight - SCREENSHOT_HEIGHT) / 2,
left: (windowWidth - SCREENSHOT_WIDTH) / 2,
height: SCREENSHOT_HEIGHT,
width: SCREENSHOT_WIDTH
});
this.setState(this._getCenteredState());
}, 0);
}
_getCenteredState() {
// TODO: Observe DOM and recenter
const { windowWidth, windowHeight } = Utils.getWindowSize();
return {
top: (windowHeight - SCREENSHOT_HEIGHT) / 2,
left: (windowWidth - SCREENSHOT_WIDTH) / 2,
height: SCREENSHOT_HEIGHT,
width: SCREENSHOT_WIDTH
};
}
render() {
return (
<div
@ -33,21 +45,27 @@ export default class FocusedSkin extends React.Component {
position: "fixed",
height: this.state.height,
width: this.state.width,
transform: `translateX(${this.state.left}px) translateY(${
this.state.top
}px)`,
transform: `translateX(${Math.round(
this.state.left
)}px) translateY(${Math.round(this.state.top)}px)`,
transition:
"transform 400ms ease-out, height 400ms ease-out, width 400ms ease-out"
}}
>
<img
style={{
width: "100%",
height: "100%"
}}
src={Utils.screenshotUrlFromHash(this.props.hash)}
<WebampComponent
key={this.props.hash} // Don't reuse instances
skinUrl={Utils.skinUrlFromHash(this.props.hash)}
screenshotUrl={Utils.screenshotUrlFromHash(this.props.hash)}
/>
)}
</div>
);
}
}
const mapStateToProps = state => ({
hash: Selectors.getSelectedSkinHash(state),
initialPosition: Selectors.getSelectedSkinPosition(state)
});
export default connect(mapStateToProps)(FocusedSkin);

View file

@ -1,18 +1,75 @@
import React from "react";
import ReactDOM from "react-dom";
import { connect } from "react-redux";
export class Overlay extends React.Component {
class Overlay extends React.Component {
constructor() {
super();
this._node = document.createElement("div");
this._node.classList.add("overlay");
this._handleKeyDown = this._handleKeyDown.bind(this);
this._handleClick = this._handleClick.bind(this);
window.document.body.appendChild(this._node);
}
componentDidMount() {
window.document.addEventListener("keydown", this._handleKeyDown);
document.body.classList.add("overlay-open");
}
componentWillUnmount() {
window.document.body.removeChild(this._node);
window.document.removeEventListener("keydown", this._handleKeyDown);
document.body.classList.remove("overlay-open");
}
_handleKeyDown(e) {
// Esc
if (e.keyCode === 27) {
this.props.closeModal();
}
}
_handleClick(e) {
if (e.target === e.currentTarget) {
this.props.closeModal();
}
}
render() {
return ReactDOM.createPortal(this.props.children, this._node);
return ReactDOM.createPortal(
<div
style={{
width: "100%",
height: "100%"
}}
onClick={this._handleClick}
onScroll={e => e.stopPropagation()}
>
<a
id="close-modal"
href="/"
onClick={e => {
this.props.closeModal();
e.preventDefault();
}}
aria-label="Close Modal"
>
&times;
</a>
{this.props.children}
</div>,
this._node
);
}
}
const mapDispatchToProps = dispatch => ({
closeModal() {
dispatch({ type: "CLOSE_MODAL" });
}
});
export default connect(
null,
mapDispatchToProps
)(Overlay);

View file

@ -36,7 +36,8 @@ export default class Skin extends React.Component {
width: this.props.width,
height: this.props.height,
top: this.props.top,
left: this.props.left
left: this.props.left,
cursor: "pointer"
}}
>
<img

67
src/WebampComponent.js Normal file
View file

@ -0,0 +1,67 @@
import React from "react";
export default class WebampComponent extends React.Component {
constructor(props) {
super(props);
this.state = { loading: true };
}
componentDidMount() {
this._loadWebamp();
}
componentWillUnmount() {
this._unmounted = true;
if (this._webamp) {
// TODO: Repace this with this._webamp.destroy() once we upgrade.
const close = document.querySelector("#webamp #close");
close.click();
}
}
async _loadWebamp() {
let Webamp;
Webamp = await import("webamp");
if (this._unmounted === true) {
return;
}
this._webamp = new Webamp({
initialSkin: {
url: this.props.skinUrl
},
initialTracks: [
{
metaData: {
artist: "DJ Mike Llama",
title: "Llama Whippin' Intro"
},
url: "/llama.mp3",
duration: 5.322286
}
],
hotkeys: true,
zIndex: 99999
});
setTimeout(async () => {
await this._webamp.renderWhenReady(this._ref);
}, 400);
if (this._unmounted === true) {
return;
}
// this.setState({ loading: false });
}
render() {
return (
<div
style={{ width: "100%", height: "100%" }}
ref={node => (this._ref = node)}
>
<img
style={{ width: "100%", height: "100%" }}
src={this.props.screenshotUrl}
/>
</div>
);
}
}

View file

@ -1,5 +0,0 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

View file

@ -1,8 +1,14 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import { Provider } from "react-redux";
import { createStore } from "./redux/store";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(
<Provider store={createStore()}>
<App />
</Provider>,
document.getElementById("root")
);
registerServiceWorker();

27
src/redux/selectors.js Normal file
View file

@ -0,0 +1,27 @@
import * as Utils from "../utils";
export function getSelectedSkinHash(state) {
return state.selectedSkinHash;
}
export function getSelectedSkinPosition(state) {
return state.selectedSkinPosition;
}
export function getSelectedSkinUrl(state) {
const hash = getSelectedSkinHash(state);
return hash == null ? null : Utils.screenshotUrlFromHash(hash);
}
export function getUrl(state) {
if (state.selectedSkinHash) {
// TODO: Add a human readable version
return `/skin/${getSelectedSkinHash(state)}/`;
} else {
return "/";
}
}
export function getPageTitle(state) {
return "Winamp Skins";
}

55
src/redux/store.js Normal file
View file

@ -0,0 +1,55 @@
import { createStore as createReduxStore } from "redux";
import * as Selectors from "./selectors";
const defaultState = {
selectedSkinHash: null,
selectedSkinPosition: null
};
function reducer(state = defaultState, action) {
switch (action.type) {
case "SELECT_SKIN":
return {
...state,
selectedSkinHash: action.hash,
selectedSkinPosition: action.position
};
case "CLOSE_MODAL":
return {
...state,
selectedSkinHash: null,
selectedSkinPosition: null
};
case "URL_CHANGED":
if (action.location.pathname.startsWith("/skin/")) {
const segments = action.location.pathname.split("/");
return {
...state,
selectedSkinHash: segments[2],
// TODO: This makes no sense
selectedSkinPosition: null
};
}
return defaultState;
default:
return state;
}
}
export function createStore() {
const store = createReduxStore(reducer);
let lastUrl = null;
store.subscribe(() => {
const state = store.getState();
const url = Selectors.getUrl(state);
if (url != lastUrl) {
window.history.pushState({}, Selectors.getPageTitle(state), url);
lastUrl = url;
}
});
window.onpopstate = function(event) {
const { state } = event;
store.dispatch({ type: "URL_CHANGED", location: document.location });
};
store.dispatch({ type: "URL_CHANGED", location: document.location });
return store;
}

View file

@ -2,6 +2,9 @@ export function screenshotUrlFromHash(hash) {
return `https://s3.amazonaws.com/webamp-uploaded-skins/screenshots/${hash}.png`;
}
export function skinUrlFromHash(hash) {
return `https://s3.amazonaws.com/webamp-uploaded-skins/skins/${hash}.wsz`;
}
export function getWindowSize() {
var w = window,
d = document,

682
yarn.lock

File diff suppressed because it is too large Load diff