diff --git a/htdocs/map-google.html b/htdocs/map-google.html
index ff73ec73..461f9539 100644
--- a/htdocs/map-google.html
+++ b/htdocs/map-google.html
@@ -9,6 +9,7 @@
+
diff --git a/htdocs/map-leaflet.html b/htdocs/map-leaflet.html
index 9251cc93..078909b4 100644
--- a/htdocs/map-leaflet.html
+++ b/htdocs/map-leaflet.html
@@ -9,6 +9,7 @@
+
diff --git a/htdocs/plugins.js b/htdocs/plugins.js
new file mode 100644
index 00000000..99850da3
--- /dev/null
+++ b/htdocs/plugins.js
@@ -0,0 +1,136 @@
+/**
+ * OpenWebRx+ Plugin loader
+ *
+ * You should load your plugins in "plugins/{type}/init.js",
+ * where {type} is 'receiver' or 'map'.
+ * See the init.js.sample.
+ * And check the "plugins/{type}/example" folder for example plugin.
+ */
+
+// Wait for the page to load, then load the plugins.
+$(document).ready(function () {
+ // detect which plugins to load
+ Plugins._type = (typeof mapManager !== 'undefined') ? 'map' : 'receiver';
+ Plugins.init();
+});
+
+// Initialize the Plugins class and some defaults
+function Plugins () {}
+Plugins._initialized = false;
+Plugins._version = 0.1; // version of the plugin sub-system (keep it float)
+Plugins._enable_debug = false; // print debug to the console
+
+
+// Load plugin
+Plugins.load = async function (name) {
+ var path = 'static/plugins/' + Plugins._type + "/" + name + "/";
+ var remote = false;
+
+ // check if we are loading an url
+ if (name.toLowerCase().startsWith('https://') || name.toLowerCase().startsWith('http://')) {
+ path = name;
+ name = path.split('/').pop().split('.').slice(0, -1);
+ path = path.split('/').slice(0, -1).join('/') + '/';
+ remote = true;
+ }
+
+ if (name in Plugins) {
+ console.warn(Plugins._get_banner(name) + 'is already loaded' + (Plugins[name]._script_loaded ? ' from ' + Plugins[name]._script_loaded : ''));
+ return;
+ }
+
+ Plugins._debug(Plugins._get_banner(name) + 'loading.');
+
+ var script_src = path + name + ".js";
+ var style_src = path + name + '.css';
+
+ // init plugin object with defaults
+ Plugins[name] = {
+ _version: 0,
+ _script_loaded: false,
+ _style_loaded: false,
+ _remote: remote,
+ };
+
+
+ // try to load the plugin
+ await Plugins._load_script(script_src)
+ .then(function () {
+ // plugin script loaded successfully
+ Plugins[name]._script_loaded = script_src;
+
+ // check if the plugin has init() method and execute it
+ if (typeof Plugins[name].init === 'function') {
+ if (!Plugins[name].init()) {
+ console.error(Plugins._get_banner(name) + 'cannot initialize.');
+ return;
+ }
+ }
+
+ // check if plugin has set the 'no_css', otherwise, load the plugin css style
+ if (!('no_css' in Plugins[name])) {
+ Plugins._load_style(style_src)
+ .then(function () {
+ Plugins[name]._style_loaded = style_src;
+ Plugins._debug(Plugins._get_banner(name) + 'loaded.');
+ }).catch(function () {
+ console.warn(Plugins._get_banner(name) + 'script loaded, but css not found.');
+ });
+ } else {
+ // plugin has no_css
+ Plugins._debug(Plugins._get_banner(name) + 'loaded.');
+ }
+
+ }).catch(function () {
+ // plugin cannot be loaded
+ Plugins._debug(Plugins._get_banner(name) + 'cannot be loaded (does not exist or has errors).');
+ });
+}
+
+// Check if plugin is loaded
+Plugins.isLoaded = function (name, version = 0) {
+ if (typeof Plugins[name] === 'object')
+ return (Plugins[name]._script_loaded && Plugins[name]._version >= version);
+ return false;
+}
+
+// Initialize plugin loading. We should load the init.js for the {type}. This init() is called onDomReady.
+Plugins.init = function () {
+ Plugins._debug("Loading " + Plugins._type + " plugins.");
+ // load the init.js for the {type}... user should load their plugins there.
+ Plugins._load_script('static/plugins/' + Plugins._type + "/init.js").then(function () {
+ Plugins._initialized = true;
+ }).catch(function () {
+ Plugins._debug('no plugins to load.');
+ })
+}
+
+// ---------------------------------------------------------------------------
+
+// internal utility methods
+Plugins._load_script = function (src) {
+ return new Promise(function (resolve, reject) {
+ var script = document.createElement('script');
+ script.onload = resolve;
+ script.onerror = reject;
+ script.src = src;
+ document.head.appendChild(script);
+ });
+}
+Plugins._load_style = function (src) {
+ return new Promise(function (resolve, reject) {
+ var style = document.createElement('link');
+ style.onload = resolve;
+ style.onerror = reject;
+ style.href = src;
+ style.type = 'text/css';
+ style.rel = 'stylesheet';
+ document.head.appendChild(style);
+ });
+}
+Plugins._debug = function (msg) {
+ if (Plugins._enable_debug) console.debug(msg);
+}
+Plugins._get_banner = function (name) {
+ return 'PLUGINS: ' + (Plugins[name] && Plugins[name]._remote ? '[remote]' : '[local]') + ' "' + name + '" ';
+}
diff --git a/htdocs/plugins/receiver/example/example.js b/htdocs/plugins/receiver/example/example.js
new file mode 100644
index 00000000..7c6ed717
--- /dev/null
+++ b/htdocs/plugins/receiver/example/example.js
@@ -0,0 +1,147 @@
+/*
+ * example UI plugin for OpenWebRx+
+ */
+
+// Disable CSS loading for this plugin
+Plugins.example.no_css = true;
+
+// Init function of the plugin
+Plugins.example.init = function () {
+
+ // Check if utils plugin is loaded
+ if (!Plugins.isLoaded('utils', 0.1)) {
+ console.error('Example plugin depends on "utils >= 0.1".');
+ return false;
+ }
+
+ // Listen to profile change and print the new profile name to console.
+ // NOTE: you cannot manipulate the data in events, you will need to wrap the original
+ // function if you want to manipulate data.
+ $(document).on('event:profile_changed', function (e, data) {
+ console.log('profile change event: ' + data);
+ });
+
+ // Another events:
+ // event:owrx_initialized - called when OWRX is initialized
+
+ // Server events are triggered when server sends data over the WS
+ // All server events have suffix ':before' or ':after', based on the original functoin call.
+ // :before events are before the original function call,
+ // :after events are after the original function call.
+ // Some interesting server events:
+ // server:config - server configuration
+ // server:bookmarks - the bookmarks from server
+ // server:clients - clients number change
+ // server:profiles - sdr profiles
+ // server:features - supported features
+
+
+ // Modify an existing OWRX function with utils plugin.
+ // See utils.js for documentation on wrap method.
+ // This will wrap profile changing function
+ Plugins.utils.wrap_func(
+ // function to wrap around
+ 'sdr_profile_changed',
+
+ // before callback, to be run before the original function
+ // orig = original function
+ // thisArg = thisArg for the original function
+ // args = the arguments for the original function
+ // If you call the original function here (in the before_cb), always return false,
+ // so the wrap_func() will not call it later again.
+ // example of calling the original function: orig.apply(thisArg, args);
+ function (orig, thisArg, args) {
+ console.log("Before callback for: " + orig.name);
+
+ // check if newly selected profile is the PMR profile
+ if ($('#openwebrx-sdr-profiles-listbox').find(':selected').text() === "[RTL] 446 PMR") {
+ // prevent changing to this profile
+ console.log('This profile is disabled by proxy function');
+
+ // restore the previous selected profile
+ $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
+
+ // return false to prevent execution of original function
+ return false;
+ }
+
+ // return true to allow execution of original function
+ return true;
+ },
+
+ // after callback, to be run after the original function,
+ // but only if the before callback returns true
+ // res = result of the original function, if any
+ function (res) {
+ console.log('profile changed.');
+ }
+ );
+
+ // this example will do the same (stop profile changing), but using another method
+ // replace the "onchange" handler of the profiles selectbox
+ // and call the original function "sdr_profile_changed"
+ $('#openwebrx-sdr-profiles-listbox')[0].onchange = function (e) {
+
+ // check the index of the selected profile (0 is the first profile in the list)
+ if (e.target.options.selectedIndex === 0) {
+ // prevent changing to this profile
+ console.log('This profile is disabled by onchange.');
+
+ // restore the previous profile
+ $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ }
+
+ // otherwise, call the original function
+ sdr_profile_changed();
+ };
+
+ // this example will manipulate bookmarks data when the server sends the bookmarks
+ // We will wrap the bookmarks.replace_bookmarks() function, once OWRX is initialized.
+ // We cannot wrap the replace_bookmarks() function before the bookmarks object is created.
+ // So we wait for OWRX to initialize and then wrap the function.
+ $(document).on('event:owrx_initialized', function () {
+
+ // Call the wrap method of utils plugin
+ Plugins.utils.wrap_func(
+
+ // function to wrap
+ 'replace_bookmarks',
+
+ // before callback
+ function (orig, thisArg, args) {
+
+ // check if the bookmarks are "server bookmarks"
+ if (args[1] === 'server') {
+
+ // check if we have array of bookmarks (will be empty if the profile has no bookmarks to show)
+ if (typeof (args[0]) === 'object' && args[0].length)
+
+ // replace the name of the first bookmark
+ args[0][0].name = 'replaced';
+ }
+
+ // now call the original function
+ orig.apply(thisArg, args);
+
+ // and return false, so the wrap_func() will not call the original for second time
+ return false;
+ },
+
+ // after callback
+ function (res) {
+ /* not executed because the before function returns false always */
+ },
+
+ // this is the object, where the repalce_boomarks() function should be found
+ bookmarks
+ );
+ });
+
+
+
+ // return true for plugin init()
+ return true;
+} // end of init function
diff --git a/htdocs/plugins/receiver/example_theme/example_theme.css b/htdocs/plugins/receiver/example_theme/example_theme.css
new file mode 100644
index 00000000..df578478
--- /dev/null
+++ b/htdocs/plugins/receiver/example_theme/example_theme.css
@@ -0,0 +1,12 @@
+/*
+ * colors for the new theme
+ */
+body.theme-eye-piercer {
+ --theme-color1: #ff6262;
+ --theme-color2: #ff626252;
+ --theme-gradient-color1: #ff6262;
+ --theme-gradient-color2: #ff0000;
+}
+
+
+
diff --git a/htdocs/plugins/receiver/example_theme/example_theme.js b/htdocs/plugins/receiver/example_theme/example_theme.js
new file mode 100644
index 00000000..88ad04ff
--- /dev/null
+++ b/htdocs/plugins/receiver/example_theme/example_theme.js
@@ -0,0 +1,14 @@
+/*
+ * example plugin, creating a new theme for OpenWebRx+
+ */
+
+// Add new entry in the Theme selectbox
+$('#openwebrx-themes-listbox').append(
+ $('