diff --git a/src/Disposable.js b/src/Disposable.js new file mode 100644 index 00000000..3fb27f10 --- /dev/null +++ b/src/Disposable.js @@ -0,0 +1,17 @@ +export default class Disposable { + _teardowns = []; + add(...teardowns) { + this._teardowns.push(...teardowns); + } + dispose() { + this._teardowns.forEach(teardown => { + if (typeof teardown === "function") { + teardown(); + } else if (typeof teardown.unsubscribe === "function") { + teardown.unsubscribe(); + } else if (typeof teardown.dispose === "function") { + teardown.dispose(); + } + }); + } +} diff --git a/src/FocusedSkin.js b/src/FocusedSkin.js index e89ae730..deafaed5 100644 --- a/src/FocusedSkin.js +++ b/src/FocusedSkin.js @@ -4,50 +4,85 @@ import WebampComponent from "./WebampComponent"; import * as Utils from "./utils"; import * as Selectors from "./redux/selectors"; import { SCREENSHOT_HEIGHT, SCREENSHOT_WIDTH } from "./constants"; +import { delay, first } from "rxjs/operators"; +import { Subject, combineLatest, timer } from "rxjs"; +import Disposable from "./Disposable"; class FocusedSkin 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({ loaded: false }, this._getCenteredState()); + this.state = Object.assign( + { 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(); } componentDidMount() { - setTimeout(() => { - // TODO: Observe DOM and recenter - this.setState(this._getCenteredState()); - }, 0); + if (!this.props.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); + } + + const transitionComplete = this._transitionBeginEvents.pipe(delay(500)); + + // Emit after both Webamp has loaded, and the transition is complete + const startWebampFadein = combineLatest( + this._webampLoadedEvents, + transitionComplete + ).pipe(first()); + + // 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"); + this._disposable.add(() => { + document.body.classList.remove("webamp-loaded"); + }); + }), + webampFadeinComplete.subscribe(() => { + this.setState({ loaded: true }); + }) + ); } componentWillUnmount() { - document.body.classList.remove("webamp-loaded"); + this._disposable.dispose(); } handleWebampLoaded = () => { - document.body.classList.add("webamp-loaded"); - setTimeout( - () => { - this.setState({ loaded: true }); - }, - // This matches the transition time that Webamp takes to fade in. - 400 - ); + 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, @@ -56,49 +91,63 @@ class FocusedSkin extends React.Component { } render() { const { loaded } = this.state; + const transform = `translateX(${Math.round( + this.state.left + )}px) translateY(${Math.round(this.state.top)}px)`; + return ( -