zoraxy/src/web/components/plugins.html
Toby Chui 136989f2ea Added plugin dir parameter
- Added plugin dir parameter
- Fixed critical architectural bug that effects plugin UI in production mode
- Updated implementation of embed FS routing
- Minor dark theme update
- Fixed ztnc UI bug for msgbox and confirm box
2025-03-28 21:24:18 +08:00

570 lines
17 KiB
HTML

<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;
}
.pluginAddRemoveButtons{
border-left: 1px solid var(--divider_color);
border-right: 1px solid var(--divider_color);
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.pluginAddRemoveButtons .mobileViewOnly{
display: none;
}
.selectColTitle{
font-weight: bold;
margin-bottom: 0.4em;
text-align: center;
width: 100%;
}
@media (max-width: 780px) {
.pluginAddRemoveButtons {
border-left: none;
border-right: none;
border-top: 1px solid var(--divider_color);
border-bottom: 1px solid var(--divider_color);
}
.pluginAddRemoveButtons .mobileViewOnly{
display: block;
}
.pluginAddRemoveButtons .wideViewOnly{
display: none;
}
}
</style>
<div class="standardContainer">
<div class="ui basic segment">
<h2>Plugins</h2>
<p>Add custom features to your Zoraxy!</p>
</div>
<div class="ui yellow message">
<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>
<br>
<div class="ui stackable grid">
<div class="seven wide column">
<!-- Selectable plugin list -->
<div class="selectColTitle">Unassigned Plugins</div>
<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 pluginAddRemoveButtons">
<!-- Add and Remove button -->
<div>
<div class="wideViewOnly">
<button class="ui basic red icon button removeSelectedPluginFromTagBtn" title="Remove selected plugin from tag">
<i class="left arrow icon"></i>
</button>
<br>
<button class="ui basic green icon button addSelectedPluginTotagBtn" title="Add selected plugin to tag" style="margin-top: 0.4em;">
<i class="right arrow icon"></i>
</button>
</div>
<div class="mobileViewOnly">
<button class="ui basic red icon button removeSelectedPluginFromTagBtn" title="Remove selected plugin from tag">
<i class="up arrow icon"></i>
</button>
<br>
<button class="ui basic green icon button addSelectedPluginTotagBtn" title="Add selected plugin to tag" style="margin-top: 0.4em;">
<i class="down arrow icon"></i>
</button>
</div>
</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 class="selectColTitle">Assigned Plugins</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>
<br>
<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>
<th>Plugin Name</th>
<th>Descriptions</th>
<th>Catergory</th>
<th>Action</th>
</tr></thead>
<tbody id="pluginTable">
</tbody>
</table>
</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 plugins 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;
}
$("#pluginMenu").append(`
<a class="item" tag="pluginContextWindow" pluginid="${plugin.Spec.id}">
<img style="width: 20px;" class="ui mini right spaced image" src="/api/plugins/icon?plugin_id=${plugin.Spec.id}"> ${plugin.Spec.name}
</a>
`);
enabledPluginCount++;
});
if (enabledPluginCount == 0){
$("#pluginMenu").append(`
<a class="item" style="pointer-events: none; user-select: none; opacity: 0.6;">
<i class="green circle check icon"></i> No Plugins Installed
</a>
`);
}
//Rebind events for the plugin menu
$("#pluginMenu").find(".item").each(function(){
$(this).off("click").on("click", function(event){
let tabid = $(this).attr("tag");
openTabById(tabid, $(this));
loadPluginUIContextIfAvailable();
});
});
/* 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();
function loadPluginUIContextIfAvailable(){
if(typeof(initPluginUIView) != "undefined"){
initPluginUIView();
}
}
function initiatePluginList(){
$.get(`/api/plugins/list`, function(data){
$("#pluginTable").html("");
data.forEach(plugin => {
let authorContact = plugin.Spec.author_contact;
if(!authorContact.startsWith('http')){
authorContact = `mailto:${authorContact}`;
}
let versionString = `v${plugin.Spec.version_major}.${plugin.Spec.version_minor}.${plugin.Spec.version_patch}`;
const row = `
<tr>
<td data-label="PluginName">
<h4 class="ui header">
<img src="/api/plugins/icon?plugin_id=${plugin.Spec.id}" class="ui image">
<div class="content">
${plugin.Spec.name}
<div class="sub header">${versionString} by <a href="${authorContact}" target="_blank">${plugin.Spec.author}</a></div>
</div>
</h4>
</td>
<td data-label="Descriptions">${plugin.Spec.description}<br>
<a href="${plugin.Spec.url}" target="_blank">${plugin.Spec.url}</a></td>
<td data-label="Category">${plugin.Spec.type==0?"Router":"Utilities"}</td>
<td data-label="Action">
<button onclick="getPluginInfo('${plugin.Spec.id}', this);" class="ui basic icon button" pluginid="${plugin.Spec.id}">
<i class="info circle icon"></i>
</button>
<button onclick="stopPlugin('${plugin.Spec.id}', this);" class="ui basic button pluginEnableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? '' : 'style="display:none;"'}>
<i class="red stop circle icon"></i> Stop
</button>
<button onclick="startPlugin('${plugin.Spec.id}', this);" class="ui basic button pluginDisableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? 'style="display:none;"' : ''}>
<i class="green play circle icon"></i> Start
</button>
</td>
</tr>
`;
$("#pluginTable").append(row);
});
if (data.length == 0){
$("#pluginTable").append(`
<tr>
<td colspan="4" style="text-align: center;"><i class="ui green circle check icon"></i> No plugins installed</td>
</tr>
`);
}
console.log(data);
});
}
initiatePluginList();
/* Tag Assignment */
/* Plugin Lifecycle */
function startPlugin(pluginId, btn=undefined){
if (btn) {
$(btn).html('<i class="spinner loading icon"></i> Starting');
$(btn).addClass('disabled');
}
$.cjax({
url: '/api/plugins/enable',
type: 'POST',
data: {plugin_id: pluginId},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Plugin started", true);
}
initiatePluginList();
initPluginSideMenu();
}
});
}
function stopPlugin(pluginId, btn=undefined){
if (btn) {
$(btn).html('<i class="spinner loading icon"></i> Stopping');
$(btn).addClass('disabled');
}
$.cjax({
url: '/api/plugins/disable',
type: 'POST',
data: {plugin_id: pluginId},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Plugin stopped", true);
}
initiatePluginList();
initPluginSideMenu();
}
});
}
/* Plugin information */
function getPluginInfo(pluginId, btn){
let payload = encodeURIComponent(JSON.stringify({plugin_id: pluginId}));
showSideWrapper("snippet/pluginInfo.html?t=" + Date.now() + "#" + payload);
}
</script>