This commit is contained in:
Marat Fayzullin 2023-12-17 16:54:23 -05:00
commit 980bae2c5d
10 changed files with 497 additions and 0 deletions

View File

@ -39,6 +39,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9, minimum-scale=0.9, shrink-to-fit=no, user-scalable=no, interactive-widget=overlays-content" />
<meta name="theme-color" content="#222" />
<script src="static/plugins.js"></script>
</head>
<body onload="openwebrx_init();">
<div id="webrx-page-container">

View File

@ -9,6 +9,7 @@
<script src="compiled/map-google.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
<script src="static/plugins.js"></script>
<meta charset="utf-8">
</head>
<body>

View File

@ -9,6 +9,7 @@
<script src="compiled/map-leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
<script src="static/plugins.js"></script>
<meta charset="utf-8">
</head>
<body>

136
htdocs/plugins.js Normal file
View File

@ -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 + '" ';
}

View File

@ -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

View File

@ -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;
}

View File

@ -0,0 +1,14 @@
/*
* example plugin, creating a new theme for OpenWebRx+
*/
// Add new entry in the Theme selectbox
$('#openwebrx-themes-listbox').append(
$('<option>').val(
// give it a value. you will need this for the css styles
"eye-piercer"
).text(
// lets name it
'Eye-Piercer'
)
);

View File

@ -0,0 +1,13 @@
// Plugin loader.
// First load the utils, needed for some plugins
Plugins.load('utils').then(function () {
// load local plugins
Plugins.load('example');
Plugins.load('example_theme');
Plugins.load('sort_profiles');
// load remote plugins
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/keyboard_shortcuts/keyboard_shortcuts.js');
});

View File

@ -0,0 +1,42 @@
/*
* Plugin: sort profiles by name.
*/
// do not load CSS for this plugin
Plugins.sort_profiles.no_css = true;
// Initialize the plugin
Plugins.sort_profiles.init = function () {
// Catch the event, when server sends us the profiles.
$(document).on('server:profiles:after', function (e, data) {
var sel = $('#openwebrx-sdr-profiles-listbox');
// if the list is empty, return
if (!sel[0] || !sel[0].length)
return;
var selected = sel.val();
var list = sel.find('option');
// sort the list of options, alphanumeric and ignorring the case
list.sort(function (a, b) {
return $(a).text()
.localeCompare(
$(b).text(), undefined, {
numeric: true,
sensitivity: 'base'
}
);
});
// now reset the list and fill it with the new sorted one
sel.html('').append(list);
// set the selected profile from our cached value
sel.val(selected);
});
// return true to validate plugin load
return true;
}

View File

@ -0,0 +1,130 @@
// This is the utils plugin
// It provides function wrapping method
// and some events for the rest plugins
// Disable CSS loading for this plugin
Plugins.utils.no_css = true;
// Utils plugin version
Plugins.utils._version = 0.1;
/**
* Wrap an existing function with before and after callbacks.
* @param {string} name The name of function to wrap with before and after callbacks.
* @param {function(orig, thisArg, args):boolean} before_cb Callback before original. Return true to call the original.
* @param {function(result):void} after_cb Callback after original, will receive the result of original
* @param {object} obj [optional] Object to look for function into. Default is 'window'
* @description
* - Before Callback:
* - Params:
* - orig: Original function (in case you want to call it, you have to return false to prevent second calling)
* - thisArg: local 'this' for the original function
* - args: arguments passed to the original function
* - Returns: Boolean. Return false to prevent execution of original function and the after callback.
* - After Callback:
* - Params:
* - res: Result of the original function
*
* @example
* // Using before and after callbacks.
* Plugins.utils.wrap_func('sdr_profile_changed',
* function (orig, thisArg, args) { // before callback
* console.log(orig.name);
* if (something_bad)
* console.log('This profile is disabled by proxy function');
* return false; // return false to prevent the calling of the original function and the after_cb()
* }
* return true; // always return true, to call the original function
* },
* function (res) { // after callback
* console.log(res);
* }
* );
*
* @example
* // Using only before callback and handle original.
* Plugins.utils.wrap_func('sdr_profile_changed',
* function (orig, thisArg, args) { // before callback
* // if we need to call the original in the middle of our work
* do_something_before_original();
* var res = orig.apply(thisArg, args);
* do_something_after_original(res);
* return false; // to prevent calling the original and after_cb
* },
* function (res) { // after callback
* // ignored
* }
* );
*
*/
Plugins.utils.wrap_func = function (name, before_cb, after_cb, obj = window) {
if (typeof obj[name] !== "function") {
console.error("Cannot wrap non existing function: '" + obj + '.' + name + "'");
return false;
}
var fn_original = obj[name];
var proxy = new Proxy(obj[name], {
apply: function (target, thisArg, args) {
if (before_cb(target, thisArg, args))
after_cb(fn_original.apply(thisArg, args));
}
});
obj[name] = proxy;
}
// Init utils plugin
Plugins.utils.init = function () {
// trigger some events
var send_events_for = {
'sdr_profile_changed': { // function name to proxy.
name: 'profile_changed', // [optional] event name (prepended with 'event:'). Default is function name.
data: function () { // [optional] data to send with the event (should be function).
return $('#openwebrx-sdr-profiles-listbox').find(':selected').text()
}
},
'on_ws_recv': {
handler: function (orig, thisArg, args) { // if handler exist, it will replace the before_cb
if (typeof args[0].data === 'string' && args[0].data.substr(0, 16) !== "CLIENT DE SERVER") {
try {
var json = JSON.parse(args[0].data);
$(document).trigger('server:' + json.type + ":before", [json['value']]);
} catch (e) {}
}
orig.apply(thisArg, args); // we handle original function here
if (typeof json === 'object')
$(document).trigger('server:' + json.type + ":after", [json['value']]);
return false; // do not call the after_cb
}
},
}
$.each(send_events_for, function (key, obj) {
Plugins.utils.wrap_func(key,
(typeof (obj.handler) === 'function') ?
obj.handler : function () {
return true;
},
function (res) {
var ev_data;
var ev_name = key;
if (typeof (obj.name) === 'string') ev_name = obj.name;
if (typeof (obj.data) === 'function') ev_data = obj.data(res);
$(document).trigger('event:' + ev_name, [ev_data]);
}
);
});
var interval = setInterval(function () {
if (typeof (clock) === 'undefined') return;
clearInterval(interval);
$(document).trigger('event:owrx_initialized');
}, 10);
return true;
}