mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-24 02:27:37 +00:00
216 lines
6.4 KiB
JavaScript
216 lines
6.4 KiB
JavaScript
import React, { useEffect } from "react";
|
|
import WebampComponent from "../WebampComponent";
|
|
import * as Utils from "../utils";
|
|
import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from "../constants";
|
|
import { delay, switchMap } from "rxjs/operators";
|
|
import { Subject, combineLatest, timer, fromEvent, from } from "rxjs";
|
|
import Disposable from "../Disposable";
|
|
import Metadata from "./Metadata";
|
|
import SkinReadme from "../SkinReadme";
|
|
|
|
function useSkinData({ hash, skinData, setSkinData }) {
|
|
useEffect(() => {
|
|
if (
|
|
skinData != null &&
|
|
skinData.color != null &&
|
|
skinData.fileName != null
|
|
) {
|
|
return;
|
|
}
|
|
// TODO: Move this to Epic
|
|
const subscription = from(fetch(`https://api.webamp.org/skins/${hash}`))
|
|
.pipe(switchMap((response) => response.json()))
|
|
.subscribe((body) => {
|
|
setSkinData(hash, {
|
|
md5: hash,
|
|
fileName: body.canonicalFilename,
|
|
color: body.averageColor,
|
|
nsfw: body.nsfw,
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
};
|
|
}, [hash, setSkinData, skinData]);
|
|
}
|
|
|
|
class BaseFocusedSkin extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this._disposable = new Disposable();
|
|
// TODO: Handle the case were we come from a permalink
|
|
if (this.props.initialPosition == null) {
|
|
this.state = Object.assign(
|
|
{
|
|
previewLoaded: false,
|
|
loaded: false,
|
|
transitionComplete: true,
|
|
},
|
|
this._getCenteredState()
|
|
);
|
|
} else {
|
|
this.state = {
|
|
loaded: false,
|
|
centered: false,
|
|
transitionComplete: false,
|
|
top: this.props.initialPosition.top,
|
|
left: this.props.initialPosition.left,
|
|
width: this.props.initialWidth,
|
|
height: this.props.initialHeight,
|
|
};
|
|
}
|
|
this._webampLoadedEvents = new Subject();
|
|
this._transitionBeginEvents = new Subject();
|
|
const transitionComplete = this._transitionBeginEvents.pipe(delay(500));
|
|
|
|
// Emit after both Webamp has loaded, and the transition is complete
|
|
const startWebampFadein = combineLatest(
|
|
this._webampLoadedEvents,
|
|
transitionComplete
|
|
);
|
|
|
|
// This value matches the opacity transition timing for `#webamp` in CSS
|
|
const webampFadeinComplete = startWebampFadein.pipe(delay(400));
|
|
|
|
this._disposable.add(
|
|
startWebampFadein.subscribe(() => {
|
|
document.body.classList.add("webamp-loaded");
|
|
}),
|
|
webampFadeinComplete.subscribe(() => {
|
|
this.setState({ loaded: true });
|
|
}),
|
|
() => {
|
|
document.body.classList.remove("webamp-loaded");
|
|
}
|
|
);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this._disposable.add(
|
|
fromEvent(window.document, "keydown").subscribe((e) => {
|
|
if (e.key === "ArrowRight") {
|
|
this.props.selectRelativeSkin(1);
|
|
} else if (e.key === "ArrowLeft") {
|
|
this.props.selectRelativeSkin(-1);
|
|
}
|
|
})
|
|
);
|
|
if (!this.state.centered) {
|
|
this._disposable.add(
|
|
timer(0).subscribe(() => {
|
|
// TODO: Observe DOM and recenter
|
|
this.setState(this._getCenteredState());
|
|
this._transitionBeginEvents.next(null);
|
|
})
|
|
);
|
|
} else {
|
|
this._transitionBeginEvents.next(null);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (prevProps.skinUrl !== this.props.skinUrl) {
|
|
document.body.classList.remove("webamp-loaded");
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._disposable.dispose();
|
|
}
|
|
|
|
handleWebampLoaded = () => {
|
|
this._webampLoadedEvents.next(null);
|
|
};
|
|
|
|
_getCenteredState() {
|
|
// TODO: Observe DOM and recenter
|
|
const { windowWidth, windowHeight } = Utils.getWindowSize();
|
|
return {
|
|
centered: true,
|
|
top: (windowHeight - SCREENSHOT_HEIGHT) / 2,
|
|
left: (windowWidth - SCREENSHOT_WIDTH) / 2,
|
|
height: SCREENSHOT_HEIGHT,
|
|
width: SCREENSHOT_WIDTH,
|
|
};
|
|
}
|
|
render() {
|
|
const { loaded } = this.state;
|
|
const transform = `translateX(${Math.round(
|
|
this.state.left
|
|
)}px) translateY(${Math.round(this.state.top)}px)`;
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{this.state.centered && (
|
|
<>
|
|
<div
|
|
style={{
|
|
position: "fixed",
|
|
height: SCREENSHOT_HEIGHT,
|
|
width: SCREENSHOT_WIDTH,
|
|
transform,
|
|
}}
|
|
>
|
|
<WebampComponent
|
|
key={this.props.hash} // Don't reuse instances
|
|
skinUrl={Utils.skinUrlFromHash(this.props.hash)}
|
|
loaded={this.handleWebampLoaded}
|
|
closeModal={this.props.closeModal}
|
|
/>
|
|
</div>
|
|
{this.props.fileExplorerOpen && <SkinReadme />}
|
|
</>
|
|
)}
|
|
<div
|
|
id="focused-skin"
|
|
style={{
|
|
position: "fixed",
|
|
height: this.state.height,
|
|
width: this.state.width,
|
|
transform,
|
|
transition:
|
|
"all 400ms ease-out, height 400ms ease-out, width 400ms ease-out",
|
|
}}
|
|
>
|
|
<div style={{ width: "100%", height: "100%" }}>
|
|
{loaded || (
|
|
<img
|
|
className={"focused-preview"}
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
// Webamp measure the scrollHeight of the container. Making this a
|
|
// block element ensures the parent element's scrollHeight is not
|
|
// expanded.
|
|
display: "block",
|
|
opacity:
|
|
this.state.previewLoaded ||
|
|
this.props.initialPosition != null
|
|
? 1
|
|
: 0,
|
|
transition: "opacity 0.2s ease-out",
|
|
}}
|
|
onLoad={() => this.setState({ previewLoaded: true })}
|
|
src={Utils.screenshotUrlFromHash(this.props.hash)}
|
|
alt={this.props.skinData && this.props.skinData.fileName}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Metadata
|
|
permalink={this.props.absolutePermalink}
|
|
openFileExplorer={this.props.openFileExplorer}
|
|
fileName={this.props.skinData && this.props.skinData.fileName}
|
|
hash={this.props.hash}
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default (props) => {
|
|
const { hash, skinData, gotSkinData } = props;
|
|
useSkinData({ hash, skinData, setSkinData: gotSkinData });
|
|
return <BaseFocusedSkin {...props} />;
|
|
};
|