Added UI for plugin system and upnp example

- Added wip UI for plugin tag system
- Added upnp port forwarder plugin
- Added error and fatal printout for plugins
- Optimized UI flow for plugin context window
- Added dev web server for plugin development purpose
This commit is contained in:
Toby Chui
2025-03-15 21:02:44 +08:00
parent 4a99afa2f0
commit f8270e46c2
34 changed files with 3143 additions and 14 deletions

View File

@ -45,6 +45,9 @@
function resizeIframe() {
let iframe = document.getElementById('pluginContextLoader');
let mainMenuHeight = document.getElementById('mainmenu').offsetHeight;
if (mainMenuHeight == 0){
mainMenuHeight = window.innerHeight - 198; //Fallback to window height
}
iframe.style.height = mainMenuHeight + 'px';
}
@ -57,6 +60,4 @@
//On switch over to this page, load info
resizeIframe();
}
initPluginUIView();
</script>

View File

@ -1,3 +1,69 @@
<style>
#selectablePluginList{
max-height: 300px;
overflow-y: auto;
border-radius: 0.4em;
}
#selectablePluginList .item {
cursor: pointer;
padding: 1em;
}
#selectablePluginList .item:hover {
background-color: var(--theme_bg_active);
}
#selectedTagPluginList{
max-height: 300px;
overflow-y: auto;
border-radius: 0.4em;
}
#selectedTagPluginList .item {
padding: 1em;
cursor: pointer;
}
#selectedTagPluginList .item:hover {
background-color: var(--theme_bg_active);
}
.selectablePluginItem{
position: relative;
}
.selectablePluginItem.active{
background-color: var(--theme_bg_active);
}
.selectablePluginItem .selectedIcon{
position: absolute;
right: 0.2em;
bottom: 0.2em;
display:none;
}
.selectablePluginItem.active .selectedIcon{
display: block;
}
.selectedPluginItem{
position: relative;
}
.selectedPluginItem.active{
background-color: var(--theme_bg_active);
}
.selectedPluginItem .selectedIcon{
position: absolute;
right: 0.2em;
bottom: 0.2em;
display:none;
}
.selectedPluginItem.active .selectedIcon{
display: block;
}
</style>
<div class="standardContainer">
<div class="ui basic segment">
<h2>Plugins</h2>
@ -7,6 +73,55 @@
<div class="header">Experimental Feature</div>
<p>This feature is experimental and may not work as expected. Use with caution.</p>
</div>
<h4 class="ui header">
Plugin Map
<div class="sub header">Assigning a plugin to a tag will make the plugin available to the HTTP Proxy rule with the same tag.</div>
</h4>
<div class="ui stackable grid">
<div class="seven wide column">
<!-- Selectable plugin list -->
<div id="selectablePluginList" class="ui relaxed divided list" style="border: 1px solid var(--divider_color);">
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="ui arrow up icon"></i> Select a tag to view available plugins
</div>
</div>
</div>
<div class="two wide column" style="display: flex; align-items: center; justify-content: center;">
<!-- Add and Remove button -->
<div>
<button id="removeSelectedPluginFromTagBtn" class="ui basic red icon button" title="Remove selected plugin from tag">
<i class="left arrow icon"></i>
</button>
<br>
<button id="addSelectedPluginTotagBtn" class="ui basic green icon button" title="Add selected plugin to tag" style="margin-top: 0.4em;">
<i class="right arrow icon"></i>
</button>
</div>
</div>
<div class="seven wide column">
<!-- Tag / Plugin List -->
<div class="ui fluid selection dropdown" id="pluginTagList">
<input type="hidden" name="tag">
<i class="dropdown icon"></i>
<div class="default text">Select Tag</div>
<div class="menu">
<!-- <div class="item" data-value="tag1">Tag 1</div> -->
</div>
</div>
<button class="ui basic fluid button" onclick="loadTags();" style="margin-top: 0.4em;"><i class="ui green refresh icon"></i> Refresh Tag List</button>
<div class="ui divider"></div>
<div id="selectedTagPluginList" class="ui relaxed divided list" style="border: 1px solid var(--divider_color);">
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="ui arrow up icon"></i> Select a tag to view assigned plugins
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<h4 class="ui header">
Plugin List
<div class="sub header">A list of installed plugins and their enable state</div>
</h4>
<table class="ui basic celled table">
<thead>
<tr>
@ -22,11 +137,220 @@
</div>
<script>
var plugin_list = [];
/* Plugin Tag Assignment */
$('#pluginTagList').dropdown();
$('#pluginTagList').on('change', function() {
const selectedTag = $(this).dropdown('get value');
loadPluginsForTag(selectedTag);
});
function loadPluginsForTag(tag) {
$.get(`/api/plugins/groups/list?tag=${tag}`, function(data) {
$("#selectedTagPluginList").html("");
let selectedPluginIDs = [];
data.forEach(plugin => {
$("#selectedTagPluginList").append(`
<div class="item selectedPluginItem" pluginid="${plugin.Spec.id}">
<img class="ui avatar image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}">
<div class="content">
<a class="header">${plugin.Spec.name}</a>
<div class="description">${plugin.Spec.description}</div>
</div>
<div class="selectedIcon">
<i class="ui large green circle check icon"></i>
</div>
</div>
`);
selectedPluginIDs.push(plugin.Spec.id);
});
if (data.length == 0){
$("#selectedTagPluginList").append(`
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="ui green circle check icon"></i> No plugins assigned to this tag
</div>
`);
}
//Load the remaining plugins to the selectable list
$("#selectablePluginList").html("");
let selectablePluginCount = 0;
plugin_list.forEach(plugin => {
if (plugin.Spec.type != 0) {
//This is not a router plugin, skip
return;
}
if (!selectedPluginIDs.includes(plugin.Spec.id)) {
$("#selectablePluginList").append(`
<div class="item selectablePluginItem" pluginid="${plugin.Spec.id}">
<img class="ui avatar image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}">
<div class="content">
<a class="header">${plugin.Spec.name}</a>
<div class="description">${plugin.Spec.description}</div>
</div>
<div class="selectedIcon">
<i class="ui large green circle check icon"></i>
</div>
</div>
`);
selectablePluginCount++;
}
});
if (selectablePluginCount == 0){
$("#selectablePluginList").append(`
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="ui green circle check icon"></i> No plugins available to assign
</div>
`);
}
bindEventsToSelectableItems();
});
}
//Load all the tags from the server
function loadTags(){
$.get(`/api/proxy/listTags`, function(data){
$("#pluginTagList").find(".menu").html("");
if (data.error != undefined){
msgbox(data.error, false);
return;
}
$("#pluginTagList").find(".menu").html("");
data.forEach(tag => {
$("#pluginTagList").find(".menu").append(`
<div class="item" data-value="${tag}">${tag}</div>
`);
});
});
}
loadTags();
//This is used as a dummy function to initialize the selectable plugin list
function initSelectablePluginList(){
$("#selectablePluginList").html("");
$.get(`/api/plugins/list`, function(data){
data.forEach(plugin => {
if (plugin.Spec.type != 0) {
//This is not a router plugin, skip
return;
}
$("#selectablePluginList").append(`
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<img class="ui avatar image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}">
<div class="content">
<a class="header">${plugin.Spec.name}</a>
<div class="description">${plugin.Spec.description}</div>
</div>
</div>
`);
});
if (data.length == 0){
$("#selectablePluginList").append(`
<div class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<p><i class="ui green circle check icon"></i> No plugins available to assign</p>
<p>Plugins can be installed to Zoraxy by placing the plugin files in the <code>./plugins/{plugin_name}/</code> directory.</p>
</div>
`);
}
});
}
initSelectablePluginList();
function bindEventsToSelectableItems(){
$(".selectablePluginItem").on("click", function(){
$(".selectablePluginItem.active").removeClass("active");
$(this).addClass("active");
});
$(".selectedPluginItem").on("click", function(){
$(".selectedPluginItem.active").removeClass("active");
$(this).addClass("active");
});
}
//Bind events for the buttons
function bindTagAssignButtonEvents(){
$("#addSelectedPluginTotagBtn").on("click", function(){
const selectedPlugin = $(".selectablePluginItem.active");
const selectedTag = $("#pluginTagList").dropdown("get value");
if (selectedPlugin.length == 0){
msgbox("Please select a plugin to add", false);
return;
}
if (selectedTag == ""){
msgbox("Please select a tag to add the plugin to", false);
return;
}
const pluginId = selectedPlugin.attr("pluginid");
addPluginToTag(pluginId, selectedTag);
});
$("#removeSelectedPluginFromTagBtn").on("click", function(){
const selectedPlugin = $(".selectedPluginItem.active");
const selectedTag = $("#pluginTagList").dropdown("get value");
if (selectedPlugin.length == 0){
msgbox("Please select a plugin to remove", false);
return;
}
if (selectedTag == ""){
msgbox("Please select a tag to remove the plugin from", false);
return;
}
const pluginId = selectedPlugin.attr("pluginid");
removePluginFromTag(pluginId, selectedTag);
});
}
bindTagAssignButtonEvents();
function addPluginToTag(pluginId, tag){
$.cjax({
url: '/api/plugins/groups/add',
type: 'POST',
data: {plugin_id: pluginId, tag: tag},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Plugin added to tag", true);
}
loadPluginsForTag(tag);
}
});
}
function removePluginFromTag(pluginId, tag){
$.cjax({
url: '/api/plugins/groups/remove',
type: 'POST',
data: {plugin_id: pluginId, tag: tag},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Plugin removed from tag", true);
}
loadPluginsForTag(tag);
}
});
}
/* Plugin List */
//Render the plugin list to Zoraxy homepage side menu
function initPluginSideMenu(){
$.get(`/api/plugins/list`, function(data){
$("#pluginMenu").html("");
let enabledPluginCount = 0;
plugin_list = data;
data.forEach(plugin => {
if (!plugin.Enabled){
return;
@ -55,6 +379,20 @@ function initPluginSideMenu(){
});
});
/* Handling Plugin Manager State, see index.html */
//Callback to be called when the plugin list is updated
if (plugin_manager_state && !plugin_manager_state.initated){
plugin_manager_state.initated = true;
if (plugin_manager_state.initCallback){
plugin_manager_state.initCallback();
}
}
//Callback to be called when the plugin list is updated
if (plugin_manager_state && plugin_manager_state.listUpdateCallback){
plugin_manager_state.listUpdateCallback();
}
});
}
initPluginSideMenu();
@ -119,6 +457,12 @@ function initiatePluginList(){
initiatePluginList();
/* Tag Assignment */
/* Plugin Lifecycle */
function startPlugin(pluginId, btn=undefined){
if (btn) {
$(btn).html('<i class="spinner loading icon"></i> Starting');

View File

@ -209,7 +209,20 @@
<br><br>
<script>
$(".year").text(new Date().getFullYear());
/*
Plugin Manager State
As some initiation must be done before the plugin manager
loaded up the plugin list, this state here tells the plugin
manager to do some actions after the plugin list is initiated
*/
var plugin_manager_state = {
initated: false, //Whether the plugin manager has been initiated
initCallback: undefined, //Callback to be called when the plugin manager is initiated
listUpdateCallback: undefined //Callback to be called when the plugin list is updated
}
/*
Loader function
@ -258,13 +271,22 @@
try {
let parsedData = JSON.parse(tabID);
tabID = parsedData.tabID;
//Open the plugin context window
if (tabID == "pluginContextWindow"){
let pluginID = parsedData.pluginID;
let button = $("#pluginMenu").find(`[pluginid="${pluginID}"]`);
openTabById(tabID, button);
loadPluginUIContextIfAvailable();
if (pluginID == undefined){
//Do not swap page
return;
}
if (!openPluginTabByID(pluginID)){
//Let the plugin manager to load the plugin list first
plugin_manager_state.initCallback = function(){
let pid = pluginID;
openPluginTabByID(pid);
}
}
}
} catch (e) {
console.error("Invalid JSON data:", e);
@ -290,6 +312,17 @@
$('table').tablesort();
});
//Function to open a plugin tab by its plugin id
function openPluginTabByID(pluginID, tabID="pluginContextWindow"){
let button = $("#pluginMenu").find(`[pluginid="${pluginID}"]`);
if (button.length == 0){
return false;
}
openTabById(tabID, button);
loadPluginUIContextIfAvailable();
return true;
}
function logout() {
$.get("/api/auth/logout", function(response) {
if (response === "OK") {
@ -495,6 +528,9 @@
});
$("body").css("overflow", "auto");
}
/* Utilities */
$(".year").text(new Date().getFullYear());
</script>
</body>
</html>