From da792c16b93978d5620aa5491da17576dff44ddd Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 3 Apr 2018 06:50:06 -0700 Subject: [PATCH] Add onClose method --- CHANGELOG.md | 1 + docs/usage.md | 17 +++++++++++++++++ js/emitter.js | 22 ++++++++++++++++++++++ js/media/elementSource.js | 34 +++++++++++----------------------- js/store.js | 18 ++++++++++++++++-- js/winamp.js | 15 +++++++++++++-- 6 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 js/emitter.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 73397bf6..52818ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## (next version) * Deprecated: The misspelled `Winamp` construction option `avaliableSkins` has been deprecated in favor of `availableSkins`. `avaliableSkins` will continue to work, but will log a deprecation warning. [#533](https://github.com/captbaritone/winamp2-js/pull/533) by [@remigallego](https://github.com/remigallego) +* Added: `winamp.onClose()`. ## 0.0.6 diff --git a/docs/usage.md b/docs/usage.md index 78c796b9..9b172035 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -193,6 +193,19 @@ winamp.renderWhenReady(container).then(() => { }); ``` +### `onClose(callback)` + +A callback which will be called when Winamp2-js is closed. Returns an "unsubscribe" function. + +```JavaScript +const unsubscribe = winamp.onClose(() => { + console.log("Winamp closed"); +}); + +// If at some point in the future you want to stop listening to these events: +unsubscribe(); +``` + ## Notes * Internet Explorer is not supported. @@ -200,3 +213,7 @@ winamp.renderWhenReady(container).then(() => { * Winamp2-js' HTML contains somewhat generic IDs and class names. If you have CSS on your page that is not namespaced, it may accidently be applied to Winamp2-js. If this happens please reach out. I may be able to resolve it. * Skin and audio URLs are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). Please ensure they are either served from the same domain, or that the other domain is served with the correct headers. * Please reach out to me. I'd love to help you set it up, and understand how it's being used. I plan to expand this API as I learn how people want to use it. + +``` + +``` diff --git a/js/emitter.js b/js/emitter.js new file mode 100644 index 00000000..dc37e66f --- /dev/null +++ b/js/emitter.js @@ -0,0 +1,22 @@ +export default class Emitter { + on(event, callback) { + const eventListeners = this._listeners[event] || []; + eventListeners.push(callback); + this._listeners[event] = eventListeners; + const unsubscribe = () => { + this._listeners[event] = eventListeners.filter(cb => cb !== callback); + }; + return unsubscribe; + } + + trigger(event) { + const callbacks = this._listeners[event]; + if (callbacks) { + callbacks.forEach(cb => cb()); + } + } + + constructor() { + this._listeners = {}; + } +} diff --git a/js/media/elementSource.js b/js/media/elementSource.js index 08a1a30e..aa07b5bd 100644 --- a/js/media/elementSource.js +++ b/js/media/elementSource.js @@ -1,3 +1,4 @@ +import Emitter from "../emitter"; const STATUS = { PLAYING: "PLAYING", STOPPED: "STOPPED", @@ -5,25 +6,12 @@ const STATUS = { }; export default class ElementSource { - on(event, callback) { - const eventListeners = this._listeners[event] || []; - eventListeners.push(callback); - this._listeners[event] = eventListeners; - const unsubscribe = () => { - this._listeners[event] = eventListeners.filter(cb => cb !== callback); - }; - return unsubscribe; - } - - trigger(event) { - const callbacks = this._listeners[event]; - if (callbacks) { - callbacks.forEach(cb => cb()); - } + on(eventType, cb) { + return this._emitter.on(eventType, cb); } constructor(context, destination) { - this._listeners = {}; + this._emitter = new Emitter(); this._context = context; this._destination = destination; this._audio = document.createElement("audio"); @@ -36,18 +24,18 @@ export default class ElementSource { }); this._audio.addEventListener("durationchange", () => { - this.trigger("loaded"); + this._emitter.trigger("loaded"); this._setStalled(false); }); this._audio.addEventListener("ended", () => { - this.trigger("ended"); + this._emitter.trigger("ended"); this._setStatus(STATUS.STOPPED); }); // TODO: Throttle to 50 (if needed) this._audio.addEventListener("timeupdate", () => { - this.trigger("positionChange"); + this._emitter.trigger("positionChange"); }); this._audio.addEventListener("error", e => { @@ -76,7 +64,7 @@ export default class ElementSource { // Rather than just geting stuck in this error state, we can just pretend this is // the end of the track. - this.trigger("ended"); + this._emitter.trigger("ended"); this._setStatus(STATUS.STOPPED); }); @@ -86,7 +74,7 @@ export default class ElementSource { _setStalled(stalled) { this._stalled = stalled; - this.trigger("stallChanged"); + this._emitter.trigger("stallChanged"); } disconnect() { @@ -128,7 +116,7 @@ export default class ElementSource { time = Math.min(time, this.getDuration()); time = Math.max(time, 0); this._audio.currentTime = time; - this.trigger("positionChange"); + this._emitter.trigger("positionChange"); } getStalled() { @@ -160,6 +148,6 @@ export default class ElementSource { _setStatus(status) { this._status = status; - this.trigger("statusChange"); + this._emitter.trigger("statusChange"); } } diff --git a/js/store.js b/js/store.js index 5cb7b799..41526165 100644 --- a/js/store.js +++ b/js/store.js @@ -11,7 +11,7 @@ const compose = composeWithDevTools({ actionsBlacklist: [UPDATE_TIME_ELAPSED, STEP_MARQUEE] }); -const getStore = (media, stateOverrides) => { +const getStore = (media, actionEmitter, stateOverrides) => { let initialState; if (stateOverrides) { initialState = merge( @@ -19,10 +19,24 @@ const getStore = (media, stateOverrides) => { stateOverrides ); } + + // eslint-disable-next-line no-unused-vars + const emitterMiddleware = store => next => action => { + actionEmitter.trigger(action.type); + return next(action); + }; + return createStore( reducer, initialState, - compose(applyMiddleware(thunk, mediaMiddleware(media), analyticsMiddleware)) + compose( + applyMiddleware( + thunk, + mediaMiddleware(media), + emitterMiddleware, + analyticsMiddleware + ) + ) ); }; diff --git a/js/winamp.js b/js/winamp.js index cab45d3c..9b56d41f 100644 --- a/js/winamp.js +++ b/js/winamp.js @@ -13,8 +13,10 @@ import { LOAD_STYLE } from "./constants"; import { SET_AVAILABLE_SKINS, NETWORK_CONNECTED, - NETWORK_DISCONNECTED + NETWORK_DISCONNECTED, + CLOSE_WINAMP } from "./actionTypes"; +import Emitter from "./emitter"; // Return a promise that resolves when the store matches a predicate. const storeHas = (store, predicate) => @@ -42,6 +44,7 @@ class Winamp { } constructor(options) { + this._actionEmitter = new Emitter(); this.options = options; const { initialTracks, @@ -51,7 +54,11 @@ class Winamp { } = this.options; this.media = new Media(); - this.store = getStore(this.media, this.options.__initialState); + this.store = getStore( + this.media, + this._actionEmitter, + this.options.__initialState + ); this.store.dispatch({ type: navigator.onLine ? NETWORK_CONNECTED : NETWORK_DISCONNECTED }); @@ -94,6 +101,10 @@ class Winamp { this.store.dispatch(loadMediaFiles(tracks, LOAD_STYLE.PLAY)); } + onClose(cb) { + return this._actionEmitter.on(CLOSE_WINAMP, cb); + } + async renderWhenReady(node) { // Wait for the skin to load. await storeHas(this.store, state => !state.display.loading);