Inject lazy dependencies (#639)

Still TODO Find a nice way to expose WebampLazy as:

```JavaScript
import WebampLazy from 'webamp/lazy';
```
This commit is contained in:
Jordan Eldredge 2018-09-02 09:01:51 -07:00 committed by GitHub
parent 6591c98b40
commit 1bdb7d6345
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 324 additions and 273 deletions

View file

@ -7,7 +7,8 @@ module.exports = {
node: {
// Consider suggesting jsmediatags use: https://github.com/feross/is-buffer
// Cuts 22k
Buffer: false
Buffer: false,
fs: "empty"
},
module: {
rules: [
@ -64,7 +65,9 @@ module.exports = {
],
entry: {
bundle: "./js/webamp.js",
"bundle.min": "./js/webamp.js"
"bundle.min": "./js/webamp.js",
"lazy-bundle": "./js/webampLazy.js",
"lazy-bundle.min": "./js/webampLazy.js"
},
output: {
path: path.resolve(__dirname, "../built"),

View file

@ -80,10 +80,23 @@ export function loadFilesFromReferences(
}
export function setSkinFromArrayBuffer(arrayBuffer) {
return async dispatch => {
return async (dispatch, getState, { requireJSZip }) => {
if (!requireJSZip) {
alert("Webamp has not been configured to support custom skins.");
return;
}
dispatch({ type: LOADING });
let JSZip;
try {
const skinData = await skinParser(arrayBuffer);
JSZip = await requireJSZip();
} catch (e) {
console.error(e);
dispatch({ type: LOADED });
alert("Failed to load the skin parser.");
return;
}
try {
const skinData = await skinParser(arrayBuffer, JSZip);
dispatch({
type: SET_SKIN_DATA,
skinImages: skinData.images,
@ -250,10 +263,10 @@ function queueFetchingMediaTags(id) {
}
export function fetchMediaTags(file, id) {
return async dispatch => {
return async (dispatch, getState, { requireJSMediaTags }) => {
dispatch({ type: MEDIA_TAG_REQUEST_INITIALIZED, id });
try {
const data = await genMediaTags(file);
const data = await genMediaTags(file, await requireJSMediaTags());
// There's more data here, but we don't have a use for it yet:
// https://github.com/aadsm/jsmediatags#shortcuts
const { artist, title, picture } = data.tags;

View file

@ -1,6 +1,6 @@
import invariant from "invariant";
export function genMediaTags(file) {
export function genMediaTags(file, jsmediatags) {
invariant(
file != null,
"Attempted to get the tags of media file without passing a file"
@ -9,26 +9,22 @@ export function genMediaTags(file) {
if (typeof file === "string" && !/^[a-z]+:\/\//i.test(file)) {
file = `${location.protocol}//${location.host}${location.pathname}${file}`;
}
return new Promise((resolve, reject) => {
require.ensure(
["jsmediatags/dist/jsmediatags"],
require => {
const jsmediatags = require("jsmediatags/dist/jsmediatags");
try {
jsmediatags.read(file, { onSuccess: resolve, onError: reject });
} catch (e) {
// Possibly jsmediatags could not find a parser for this file?
// Nothing to do.
// Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved.
reject(e);
}
},
() => {
// The dependency failed to load
},
"jsmediatags"
);
});
return new Promise(
(resolve, reject) => {
try {
jsmediatags.read(file, { onSuccess: resolve, onError: reject });
} catch (e) {
// Possibly jsmediatags could not find a parser for this file?
// Nothing to do.
// Consider removing this after https://github.com/aadsm/jsmediatags/issues/83 is resolved.
reject(e);
}
},
() => {
// The dependency failed to load
},
"jsmediatags"
);
}
export function genMediaDuration(url) {

View file

@ -12,7 +12,7 @@ import zaxon from "../skins/ZaxonRemake1-0.wsz";
import green from "../skins/Green-Dimension-V2.wsz";
import MilkdropWindow from "./components/MilkdropWindow";
import screenshotInitialState from "./screenshotInitialState";
import Webamp from "./webamp";
import WebampLazy from "./webampLazy";
import {
STEP_MARQUEE,
UPDATE_TIME_ELAPSED,
@ -34,6 +34,38 @@ import {
disableMarquee
} from "./config";
const requireJSZip = () => {
return new Promise((resolve, reject) => {
require.ensure(
["jszip/dist/jszip"],
require => {
resolve(require("jszip/dist/jszip"));
},
e => {
console.error("Error loading JSZip", e);
reject(e);
},
"jszip"
);
});
};
const requireJSMediaTags = () => {
return new Promise((resolve, reject) => {
require.ensure(
["jsmediatags/dist/jsmediatags"],
require => {
resolve(require("jsmediatags/dist/jsmediatags"));
},
e => {
console.error("Error loading jsmediatags", e);
reject(e);
},
"jsmediatags"
);
});
};
const NOISY_ACTION_TYPES = new Set([
STEP_MARQUEE,
UPDATE_TIME_ELAPSED,
@ -122,7 +154,7 @@ Raven.context(() => {
if (screenshot) {
document.getElementsByClassName("about")[0].style.visibility = "hidden";
}
if (!Webamp.browserIsSupported()) {
if (!WebampLazy.browserIsSupported()) {
document.getElementById("browser-compatibility").style.display = "block";
document.getElementById("app").style.visibility = "hidden";
return;
@ -165,7 +197,7 @@ Raven.context(() => {
const initialSkin = !skinUrl ? null : { url: skinUrl };
const webamp = new Webamp({
const webamp = new WebampLazy({
initialSkin,
initialTracks: screenshot ? null : initialTracks,
availableSkins: [
@ -191,6 +223,8 @@ Raven.context(() => {
}
],
enableHotkeys: true,
requireJSZip,
requireJSMediaTags,
__extraWindows,
__initialWindowLayout,
__initialState: screenshot ? screenshotInitialState : initialState,

View file

@ -3,21 +3,6 @@ import regionParser from "./regionParser";
import { LETTERS, DEFAULT_SKIN } from "./constants";
import { parseViscolors, parseIni, getFileExtension } from "./utils";
const getJSZip = () => {
return new Promise(resolve => {
require.ensure(
["jszip/dist/jszip"],
require => {
resolve(require("jszip/dist/jszip"));
},
e => {
console.error("Error loading JSZip", e);
},
"jszip"
);
});
};
const shallowMerge = objs =>
objs.reduce((prev, img) => Object.assign(prev, img), {});
@ -249,8 +234,7 @@ async function genGenTextSprites(zip) {
}
// A promise that, given an array buffer returns a skin style object
async function skinParser(zipFileBuffer) {
const JSZip = await getJSZip();
async function skinParser(zipFileBuffer, JSZip) {
const zip = await JSZip.loadAsync(zipFileBuffer);
const [
colors,

View file

@ -10,12 +10,13 @@ const compose = composeWithDevTools({
actionsBlacklist: [UPDATE_TIME_ELAPSED, STEP_MARQUEE]
});
const getStore = (
export default function(
media,
actionEmitter,
customMiddlewares = [],
stateOverrides
) => {
stateOverrides,
extras
) {
let initialState;
if (stateOverrides) {
initialState = merge(
@ -36,7 +37,7 @@ const getStore = (
compose(
applyMiddleware(
...[
thunk,
thunk.withExtraArgument(extras),
mediaMiddleware(media),
emitterMiddleware,
...customMiddlewares
@ -44,6 +45,4 @@ const getStore = (
)
)
);
};
export default getStore;
}

View file

@ -1,224 +1,14 @@
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import getStore from "./store";
import App from "./components/App";
import Hotkeys from "./hotkeys";
import Media from "./media";
import { getTrackCount, getTracks } from "./selectors";
import {
setSkinFromUrl,
loadMediaFiles,
setWindowSize,
loadFilesFromReferences
} from "./actionCreators";
import { LOAD_STYLE } from "./constants";
import { uniqueId, objectMap, objectForEach } from "./utils";
import {
SET_AVAILABLE_SKINS,
NETWORK_CONNECTED,
NETWORK_DISCONNECTED,
CLOSE_WINAMP,
MINIMIZE_WINAMP,
ADD_GEN_WINDOW,
UPDATE_WINDOW_POSITIONS,
LOADED,
REGISTER_VISUALIZER,
SET_Z_INDEX,
SET_MEDIA
} from "./actionTypes";
import Emitter from "./emitter";
import "../css/base-skin.min.css";
// Return a promise that resolves when the store matches a predicate.
const storeHas = (store, predicate) =>
new Promise(resolve => {
if (predicate(store.getState())) {
resolve();
return;
}
const unsubscribe = store.subscribe(() => {
if (predicate(store.getState())) {
resolve();
unsubscribe();
}
});
});
class Winamp {
static browserIsSupported() {
const supportsAudioApi = !!(
window.AudioContext || window.webkitAudioContext
);
const supportsCanvas = !!window.document.createElement("canvas").getContext;
const supportsPromises = typeof Promise !== "undefined";
return supportsAudioApi && supportsCanvas && supportsPromises;
}
import JSZip from "jszip";
import jsmediatags from "jsmediatags";
import WebampLazy from "./webampLazy";
class Winamp extends WebampLazy {
constructor(options) {
this._actionEmitter = new Emitter();
this.options = options;
const {
initialTracks,
initialSkin,
avaliableSkins, // Old misspelled name
availableSkins,
enableHotkeys = false,
zIndex,
__extraWindows
} = this.options;
this.media = new Media();
this.store = getStore(
this.media,
this._actionEmitter,
this.options.__customMiddlewares,
this.options.__initialState
);
this.store.dispatch({
type: navigator.onLine ? NETWORK_CONNECTED : NETWORK_DISCONNECTED
super({
...options,
requireJSZip: () => JSZip,
requireJSMediaTags: () => jsmediatags
});
if (true) {
const fileInput = document.createElement("input");
fileInput.id = "webamp-file-input";
fileInput.style.display = "none";
fileInput.type = "file";
fileInput.value = null;
fileInput.addEventListener("change", e => {
this.store.dispatch(loadFilesFromReferences(e.target.files));
});
document.body.appendChild(fileInput);
}
if (zIndex != null) {
this.store.dispatch({ type: SET_Z_INDEX, zIndex });
}
this.genWindows = [];
if (__extraWindows) {
this.genWindows = __extraWindows.map(genWindow => ({
id: genWindow.id || `${genWindow.title}-${uniqueId()}`,
...genWindow
}));
__extraWindows.forEach(genWindow => {
if (genWindow.isVisualizer) {
this.store.dispatch({ type: REGISTER_VISUALIZER, id: genWindow.id });
}
});
}
this.genWindows.forEach(genWindow => {
this.store.dispatch({
type: ADD_GEN_WINDOW,
windowId: genWindow.id,
title: genWindow.title,
open: genWindow.open
});
});
window.addEventListener("online", () =>
this.store.dispatch({ type: NETWORK_CONNECTED })
);
window.addEventListener("offline", () =>
this.store.dispatch({ type: NETWORK_DISCONNECTED })
);
if (initialSkin) {
this.store.dispatch(setSkinFromUrl(initialSkin.url));
} else {
// We are using the default skin.
this.store.dispatch({ type: LOADED });
}
if (initialTracks) {
this.appendTracks(initialTracks);
}
if (avaliableSkins != null) {
console.warn(
"The misspelled option `avaliableSkins` is deprecated. Please use `availableSkins` instead."
);
this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: avaliableSkins });
} else if (availableSkins != null) {
this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: availableSkins });
}
const layout = options.__initialWindowLayout;
if (layout != null) {
objectForEach(layout, (w, windowId) => {
if (w.size != null) {
this.store.dispatch(setWindowSize(windowId, w.size));
}
});
this.store.dispatch({
type: UPDATE_WINDOW_POSITIONS,
positions: objectMap(layout, w => w.position)
});
}
if (enableHotkeys) {
new Hotkeys(this.store.dispatch);
}
}
// Append this array of tracks to the end of the current playlist.
appendTracks(tracks) {
const nextIndex = getTrackCount(this.store.getState());
this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex));
}
// Replace any existing tracks with this array of tracks, and begin playing.
setTracksToPlay(tracks) {
this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY));
}
onClose(cb) {
return this._actionEmitter.on(CLOSE_WINAMP, cb);
}
onTrackDidChange(cb) {
return this._actionEmitter.on(SET_MEDIA, action => {
const tracks = getTracks(this.store.getState());
const track = tracks[action.id];
if (track == null) {
return;
}
cb({ url: track.url });
});
}
onMinimize(cb) {
return this._actionEmitter.on(MINIMIZE_WINAMP, cb);
}
async skinIsLoaded() {
// Wait for the skin to load.
return storeHas(this.store, state => !state.display.loading);
}
async renderWhenReady(node) {
await this.skinIsLoaded();
const genWindowComponents = {};
this.genWindows.forEach(w => {
genWindowComponents[w.id] = w.Component;
});
render(
<Provider store={this.store}>
<App
media={this.media}
container={node}
filePickers={this.options.filePickers}
genWindowComponents={genWindowComponents}
/>
</Provider>,
node
);
}
}

231
js/webampLazy.js Normal file
View file

@ -0,0 +1,231 @@
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import getStore from "./store";
import App from "./components/App";
import Hotkeys from "./hotkeys";
import Media from "./media";
import { getTrackCount, getTracks } from "./selectors";
import {
setSkinFromUrl,
loadMediaFiles,
setWindowSize,
loadFilesFromReferences
} from "./actionCreators";
import { LOAD_STYLE } from "./constants";
import { uniqueId, objectMap, objectForEach } from "./utils";
import {
SET_AVAILABLE_SKINS,
NETWORK_CONNECTED,
NETWORK_DISCONNECTED,
CLOSE_WINAMP,
MINIMIZE_WINAMP,
ADD_GEN_WINDOW,
UPDATE_WINDOW_POSITIONS,
LOADED,
REGISTER_VISUALIZER,
SET_Z_INDEX,
SET_MEDIA
} from "./actionTypes";
import Emitter from "./emitter";
import "../css/base-skin.min.css";
// Return a promise that resolves when the store matches a predicate.
const storeHas = (store, predicate) =>
new Promise(resolve => {
if (predicate(store.getState())) {
resolve();
return;
}
const unsubscribe = store.subscribe(() => {
if (predicate(store.getState())) {
resolve();
unsubscribe();
}
});
});
class Winamp {
static browserIsSupported() {
const supportsAudioApi = !!(
window.AudioContext || window.webkitAudioContext
);
const supportsCanvas = !!window.document.createElement("canvas").getContext;
const supportsPromises = typeof Promise !== "undefined";
return supportsAudioApi && supportsCanvas && supportsPromises;
}
constructor(options) {
this._actionEmitter = new Emitter();
this.options = options;
const {
initialTracks,
initialSkin,
avaliableSkins, // Old misspelled name
availableSkins,
enableHotkeys = false,
zIndex,
requireJSZip,
requireJSMediaTags,
__extraWindows
} = this.options;
// TODO: Validate required options
this.media = new Media();
this.store = getStore(
this.media,
this._actionEmitter,
this.options.__customMiddlewares,
this.options.__initialState,
{ requireJSZip, requireJSMediaTags }
);
this.store.dispatch({
type: navigator.onLine ? NETWORK_CONNECTED : NETWORK_DISCONNECTED
});
if (true) {
const fileInput = document.createElement("input");
fileInput.id = "webamp-file-input";
fileInput.style.display = "none";
fileInput.type = "file";
fileInput.value = null;
fileInput.addEventListener("change", e => {
this.store.dispatch(loadFilesFromReferences(e.target.files));
});
document.body.appendChild(fileInput);
}
if (zIndex != null) {
this.store.dispatch({ type: SET_Z_INDEX, zIndex });
}
this.genWindows = [];
if (__extraWindows) {
this.genWindows = __extraWindows.map(genWindow => ({
id: genWindow.id || `${genWindow.title}-${uniqueId()}`,
...genWindow
}));
__extraWindows.forEach(genWindow => {
if (genWindow.isVisualizer) {
this.store.dispatch({ type: REGISTER_VISUALIZER, id: genWindow.id });
}
});
}
this.genWindows.forEach(genWindow => {
this.store.dispatch({
type: ADD_GEN_WINDOW,
windowId: genWindow.id,
title: genWindow.title,
open: genWindow.open
});
});
window.addEventListener("online", () =>
this.store.dispatch({ type: NETWORK_CONNECTED })
);
window.addEventListener("offline", () =>
this.store.dispatch({ type: NETWORK_DISCONNECTED })
);
if (initialSkin) {
this.store.dispatch(setSkinFromUrl(initialSkin.url));
} else {
// We are using the default skin.
this.store.dispatch({ type: LOADED });
}
if (initialTracks) {
this.appendTracks(initialTracks);
}
if (avaliableSkins != null) {
console.warn(
"The misspelled option `avaliableSkins` is deprecated. Please use `availableSkins` instead."
);
this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: avaliableSkins });
} else if (availableSkins != null) {
this.store.dispatch({ type: SET_AVAILABLE_SKINS, skins: availableSkins });
}
const layout = options.__initialWindowLayout;
if (layout != null) {
objectForEach(layout, (w, windowId) => {
if (w.size != null) {
this.store.dispatch(setWindowSize(windowId, w.size));
}
});
this.store.dispatch({
type: UPDATE_WINDOW_POSITIONS,
positions: objectMap(layout, w => w.position)
});
}
if (enableHotkeys) {
new Hotkeys(this.store.dispatch);
}
}
// Append this array of tracks to the end of the current playlist.
appendTracks(tracks) {
const nextIndex = getTrackCount(this.store.getState());
this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.BUFFER, nextIndex));
}
// Replace any existing tracks with this array of tracks, and begin playing.
setTracksToPlay(tracks) {
this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY));
}
onClose(cb) {
return this._actionEmitter.on(CLOSE_WINAMP, cb);
}
onTrackDidChange(cb) {
return this._actionEmitter.on(SET_MEDIA, action => {
const tracks = getTracks(this.store.getState());
const track = tracks[action.id];
if (track == null) {
return;
}
cb({ url: track.url });
});
}
onMinimize(cb) {
return this._actionEmitter.on(MINIMIZE_WINAMP, cb);
}
async skinIsLoaded() {
// Wait for the skin to load.
return storeHas(this.store, state => !state.display.loading);
}
async renderWhenReady(node) {
await this.skinIsLoaded();
const genWindowComponents = {};
this.genWindows.forEach(w => {
genWindowComponents[w.id] = w.Component;
});
render(
<Provider store={this.store}>
<App
media={this.media}
container={node}
filePickers={this.options.filePickers}
genWindowComponents={genWindowComponents}
/>
</Provider>,
node
);
}
}
export default Winamp;
module.exports = Winamp;

View file

@ -6,7 +6,8 @@
"files": [
"built/webamp.bundle.js",
"built/webamp.bundle.min.js",
"index.d.ts"
"built/webamp-lazy.bundle.js",
"built/webamp-lazy.bundle.min.js"
],
"scripts": {
"lint": "eslint .",