mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-22 17:47:16 +00:00
Progress
This commit is contained in:
parent
e0b7a32b9d
commit
b9cacadcee
14 changed files with 377 additions and 674 deletions
|
|
@ -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
BIN
public/llama.mp3
Normal file
Binary file not shown.
38
src/App.css
38
src/App.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
32
src/App.js
32
src/App.js
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
×
|
||||
</a>
|
||||
{this.props.children}
|
||||
</div>,
|
||||
this._node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
closeModal() {
|
||||
dispatch({ type: "CLOSE_MODAL" });
|
||||
}
|
||||
});
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(Overlay);
|
||||
|
|
|
|||
|
|
@ -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
67
src/WebampComponent.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
10
src/index.js
10
src/index.js
|
|
@ -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
27
src/redux/selectors.js
Normal 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
55
src/redux/store.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue