nukeop / nuclear

Streaming music player that finds free music for you
https://nuclear.js.org/
GNU Affero General Public License v3.0
11.93k stars 1.03k forks source link

[feature request] Plugin "hook" interface #695

Open Venryx opened 4 years ago

Venryx commented 4 years ago

I'm trying to make a Nuclear plugin (repo here) which will add sliders next to each folder and track, in the new folder-tree view. (to enable changing the distribution-amount of each folder/track within the next generated playlist)

However, for my plugin to do this, there needs to be some way for it to modify the treeData structure sent to the react-redux-grid Grid component.

Now technically, there should be ways to accomplish this entirely through "monkey patching" -- for example, I could probably do something like:

const oldCreateElement = require("react").createElement;
require("react").createElement = function(...args) {
  if (args[0].name == "Grid") {
    let treeData = args[1].treeData;
    // do some modifications here
  }
  return oldCreateElement.apply(this, args);
};

However, this sort of monkey-patching may get prohibitively difficult and hard-to-follow for other modifications a plugin may want to make. (some are likely even impossible without monkey-patch-replacing entire subsystems)

So my question is: What sort of approach do you have in mind for enabling plugin access to various "deeper" parts of the software?

The idea I'm partial toward: Having a standardized "hook" system, similar to that used by the Webpack compiler chain: https://github.com/webpack/tapable#tapable

Rather than describe, I'll just show an example:

export = {
  name: "some plugin",
  onLoad: api => {
    api.hooks.preRenderFolderTree.tap(treeData=> {
      // do some modifications here
    });
  },
};

This approach is more clean/straightforward than the monkey-patch approach, and is more flexible: we can add hooks to pretty much anywhere in the software that we think it could be useful for plugins to extend.

Note that hooks are distinct from the existing api in that hooks allow plugin code to "run whenever the given procedure is in progress"; the existing api (exposing access to the redux store, etc.) is good for allowing sending "commands" into the program, but there is so far no way to "hook into" or "add listeners" to program behavior. (well, you can subscribe to the store, but I mean more fine-grained behaviors such as rendering the folder-tree UI) Anyway...

Thoughts?

nukeop commented 4 years ago

I'll have to try implementing this to see what's possible and what's the best way to do this, but my initial idea would be to create a higher order component that registers the component it modifies in the API object, and lets the plugin developer tap into its lifetime methods. This would not require a lot of effort and would be generic enough to extend to most of the program's UI.

Another useful thing would be to allow easy access to common events such as next track, play/pause, item added to queue etc. without caring about redux store.

Venryx commented 4 years ago

I'll have to try implementing this to see what's possible and what's the best way to do this

Sounds good.

Perhaps something that would be nice to add is an "action listener" system (implemented using a Redux middleware, or the reducer approach seen here); it's easier for plugin developers to listen for specific actions (for some things) than it is to subscribe to a change in the store anywhere, and then infer the action-type based on the store change that occurred.

Redux tends to encourage a codebase relying on state-change listening (eg. store.subscribe), as in one sense it's more flexible, but for "ad-hoc" usage by things like plugins, I think action-dispatch listening can be easier to use (it's higher-level for some things, doesn't require local state to detect changes, and requires less data-structure exploration).

Essentially the "action listener" system above would remove the need for this:

Another useful thing would be to allow easy access to common events such as next track, play/pause, item added to queue etc. without caring about redux store.

...since the plugin could just listen for the pre-existing "START_PLAYBACK", "PAUSE_PLAYBACK", etc. actions.

Anyway, for now I'll make do with the Redux store subscribing+dispatching, and/or the React.createElement monkey-patching approach mentioned earlier, to accomplish the folder-tree view augmentation I have planned (adding a new plugin-specific column).