mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
Added working plugin store prototype
- Added plugin install and remove api
This commit is contained in:
parent
6750c7fe3d
commit
ffc67ede12
@ -237,6 +237,8 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
|
|||||||
|
|
||||||
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
|
authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins)
|
||||||
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
|
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
|
||||||
|
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
|
||||||
|
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
|
||||||
|
@ -274,13 +274,10 @@ func (m *Manager) StopPlugin(pluginID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the plugin is still running
|
// Check if the plugin is still running
|
||||||
func (m *Manager) PluginStillRunning(pluginID string) bool {
|
func (m *Manager) PluginIsRunning(pluginID string) bool {
|
||||||
plugin, err := m.GetPluginByID(pluginID)
|
plugin, err := m.GetPluginByID(pluginID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if plugin.process == nil {
|
return plugin.IsRunning()
|
||||||
return false
|
|
||||||
}
|
|
||||||
return plugin.process.ProcessState == nil
|
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,59 @@ func NewPluginManager(options *ManagerOptions) *Manager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload all plugins from disk
|
||||||
|
func (m *Manager) ReloadPluginFromDisk() {
|
||||||
|
//Check each of the current plugins if the directory exists
|
||||||
|
//If not, remove the plugin from the loaded plugins list
|
||||||
|
m.loadedPluginsMutex.Lock()
|
||||||
|
for pluginID, plugin := range m.LoadedPlugins {
|
||||||
|
if !utils.FileExists(plugin.RootDir) {
|
||||||
|
m.Log("Plugin directory not found, removing plugin from runtime: "+pluginID, nil)
|
||||||
|
delete(m.LoadedPlugins, pluginID)
|
||||||
|
//Remove the plugin enable state from the database
|
||||||
|
m.Options.Database.Delete("plugins", pluginID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.loadedPluginsMutex.Unlock()
|
||||||
|
|
||||||
|
//Scan the plugin directory for new plugins
|
||||||
|
foldersInPluginDir, err := os.ReadDir(m.Options.PluginDir)
|
||||||
|
if err != nil {
|
||||||
|
m.Log("Failed to read plugin directory", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, folder := range foldersInPluginDir {
|
||||||
|
if folder.IsDir() {
|
||||||
|
pluginPath := filepath.Join(m.Options.PluginDir, folder.Name())
|
||||||
|
thisPlugin, err := m.LoadPluginSpec(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
m.Log("Failed to load plugin: "+filepath.Base(pluginPath), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the plugin id is already loaded into the runtime
|
||||||
|
m.loadedPluginsMutex.RLock()
|
||||||
|
_, ok := m.LoadedPlugins[thisPlugin.Spec.ID]
|
||||||
|
m.loadedPluginsMutex.RUnlock()
|
||||||
|
if ok {
|
||||||
|
//Plugin already loaded, skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
thisPlugin.RootDir = filepath.ToSlash(pluginPath)
|
||||||
|
thisPlugin.staticRouteProxy = make(map[string]*dpcore.ReverseProxy)
|
||||||
|
m.loadedPluginsMutex.Lock()
|
||||||
|
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
|
||||||
|
m.loadedPluginsMutex.Unlock()
|
||||||
|
m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil)
|
||||||
|
|
||||||
|
// The default state of the plugin is disabled, so no need to start it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
// LoadPluginsFromDisk loads all plugins from the plugin directory
|
||||||
func (m *Manager) LoadPluginsFromDisk() error {
|
func (m *Manager) LoadPluginsFromDisk() error {
|
||||||
// Load all plugins from the plugin directory
|
// Load all plugins from the plugin directory
|
||||||
@ -258,3 +311,8 @@ func (p *Plugin) HandleStaticRoute(w http.ResponseWriter, r *http.Request, longe
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRunning checks if the plugin is currently running
|
||||||
|
func (p *Plugin) IsRunning() bool {
|
||||||
|
return p.process != nil && p.process.Process != nil
|
||||||
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
package plugins
|
package plugins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -89,6 +93,180 @@ func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin {
|
|||||||
return m.Options.DownloadablePluginCache
|
return m.Options.DownloadablePluginCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallPlugin installs the given plugin by moving it to the PluginDir.
|
||||||
|
func (m *Manager) InstallPlugin(plugin *DownloadablePlugin) error {
|
||||||
|
pluginDir := filepath.Join(m.Options.PluginDir, plugin.PluginIntroSpect.Name)
|
||||||
|
pluginFile := plugin.PluginIntroSpect.Name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
pluginFile += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the plugin id already exists in runtime plugin map
|
||||||
|
if _, ok := m.LoadedPlugins[plugin.PluginIntroSpect.ID]; ok {
|
||||||
|
return fmt.Errorf("plugin already installed: %s", plugin.PluginIntroSpect.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the plugin directory if it doesn't exist
|
||||||
|
err := os.MkdirAll(pluginDir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create plugin directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the plugin binary
|
||||||
|
downloadURL, ok := plugin.DownloadURLs[runtime.GOOS+"_"+runtime.GOARCH]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no download URL available for the current platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(downloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download plugin: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to download plugin: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the plugin binary to the plugin directory
|
||||||
|
pluginPath := filepath.Join(pluginDir, pluginFile)
|
||||||
|
out, err := os.Create(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create plugin file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
out.Close()
|
||||||
|
return fmt.Errorf("failed to write plugin file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the plugin executable
|
||||||
|
err = os.Chmod(pluginPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
out.Close()
|
||||||
|
return fmt.Errorf("failed to set executable permissions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the checksum of the downloaded plugin binary
|
||||||
|
checksums, err := plugin.ChecksumsSHA256.GetCurrentPlatformChecksum()
|
||||||
|
if err == nil {
|
||||||
|
if !verifyChecksumForFile(pluginPath, checksums) {
|
||||||
|
out.Close()
|
||||||
|
return fmt.Errorf("checksum verification failed for plugin binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ok, also download the icon if exists
|
||||||
|
if plugin.IconPath != "" {
|
||||||
|
iconURL := strings.TrimSpace(plugin.IconPath)
|
||||||
|
if iconURL != "" {
|
||||||
|
resp, err := http.Get(iconURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download plugin icon: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
//Save the icon to the plugin directory
|
||||||
|
iconPath := filepath.Join(pluginDir, "icon.png")
|
||||||
|
out, err := os.Create(iconPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create plugin icon file: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
io.Copy(out, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Close the plugin exeutable
|
||||||
|
out.Close()
|
||||||
|
|
||||||
|
//Reload the plugin list
|
||||||
|
m.ReloadPluginFromDisk()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UninstallPlugin uninstalls the plugin by removing its directory.
|
||||||
|
func (m *Manager) UninstallPlugin(pluginID string) error {
|
||||||
|
|
||||||
|
//Stop the plugin process if it's running
|
||||||
|
plugin, ok := m.LoadedPlugins[pluginID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("plugin not found: %s", pluginID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin.IsRunning() {
|
||||||
|
err := m.StopPlugin(plugin.Spec.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to stop plugin: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Make sure the plugin process is stopped
|
||||||
|
m.Options.Logger.PrintAndLog("plugin-manager", "Removing plugin in 3 seconds...", nil)
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
// Remove the plugin directory
|
||||||
|
err := os.RemoveAll(plugin.RootDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove plugin directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Reload the plugin list
|
||||||
|
m.ReloadPluginFromDisk()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentPlatformChecksum returns the checksum for the current platform
|
||||||
|
func (c *Checksums) GetCurrentPlatformChecksum() (string, error) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
return c.LinuxAmd64, nil
|
||||||
|
case "386":
|
||||||
|
return c.Linux386, nil
|
||||||
|
case "arm":
|
||||||
|
return c.LinuxArm, nil
|
||||||
|
case "arm64":
|
||||||
|
return c.LinuxArm64, nil
|
||||||
|
case "mipsle":
|
||||||
|
return c.LinuxMipsle, nil
|
||||||
|
case "riscv64":
|
||||||
|
return c.LinuxRiscv64, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
case "windows":
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "amd64":
|
||||||
|
return c.WindowsAmd64, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported architecture: %s", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyChecksum verifies the checksum of the downloaded plugin binary.
|
||||||
|
func verifyChecksumForFile(filePath string, checksum string) bool {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
calculatedChecksum := fmt.Sprintf("%x", hash.Sum(nil))
|
||||||
|
|
||||||
|
return calculatedChecksum == checksum
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Handlers for Plugin Store
|
Handlers for Plugin Store
|
||||||
*/
|
*/
|
||||||
@ -116,3 +294,63 @@ func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleInstallPlugin is the handler for installing a plugin
|
||||||
|
func (m *Manager) HandleInstallPlugin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
utils.SendErrorResponse(w, "Method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginID, err := utils.PostPara(r, "pluginID")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "pluginID is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the plugin info from cache
|
||||||
|
var plugin *DownloadablePlugin
|
||||||
|
for _, p := range m.Options.DownloadablePluginCache {
|
||||||
|
if p.PluginIntroSpect.ID == pluginID {
|
||||||
|
plugin = p
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if plugin == nil {
|
||||||
|
utils.SendErrorResponse(w, "Plugin not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the plugin (implementation depends on your system)
|
||||||
|
err = m.InstallPlugin(plugin)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to install plugin: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUninstallPlugin is the handler for uninstalling a plugin
|
||||||
|
func (m *Manager) HandleUninstallPlugin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
utils.SendErrorResponse(w, "Method not allowed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginID, err := utils.PostPara(r, "pluginID")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "pluginID is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall the plugin (implementation depends on your system)
|
||||||
|
err = m.UninstallPlugin(pluginID)
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "Failed to uninstall plugin: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
}
|
||||||
|
@ -186,7 +186,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br>
|
||||||
<button class="ui violet button" onclick="openPluginStore();"><i class="cart arrow down icon"></i>Get More Plugins!</button>
|
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -482,7 +482,10 @@ function initiatePluginList(){
|
|||||||
<a href="${plugin.Spec.url}" target="_blank">${plugin.Spec.url}</a></td>
|
<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="Category">${plugin.Spec.type==0?"Router":"Utilities"}</td>
|
||||||
<td data-label="Action">
|
<td data-label="Action">
|
||||||
<button onclick="getPluginInfo('${plugin.Spec.id}', this);" class="ui basic icon button" pluginid="${plugin.Spec.id}">
|
<button onclick="uninstallPlugin('${plugin.Spec.id}', '${plugin.Spec.name}', this);" class="ui basic red icon button">
|
||||||
|
<i class="trash icon"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="getPluginInfo('${plugin.Spec.id}', this);" class="ui basic icon button" pluginid="${plugin.Spec.id}">
|
||||||
<i class="info circle icon"></i>
|
<i class="info circle icon"></i>
|
||||||
</button>
|
</button>
|
||||||
<button onclick="stopPlugin('${plugin.Spec.id}', this);" class="ui basic button pluginEnableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? '' : 'style="display:none;"'}>
|
<button onclick="stopPlugin('${plugin.Spec.id}', this);" class="ui basic button pluginEnableButton" pluginid="${plugin.Spec.id}" ${plugin.Enabled ? '' : 'style="display:none;"'}>
|
||||||
@ -511,9 +514,6 @@ function initiatePluginList(){
|
|||||||
|
|
||||||
initiatePluginList();
|
initiatePluginList();
|
||||||
|
|
||||||
/* Tag Assignment */
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Plugin Lifecycle */
|
/* Plugin Lifecycle */
|
||||||
@ -570,6 +570,28 @@ function openPluginStore(){
|
|||||||
showSideWrapper("snippet/pluginstore.html?t=" + Date.now(), true);
|
showSideWrapper("snippet/pluginstore.html?t=" + Date.now(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uninstallPlugin(pluginId, pluginName, btn=undefined) {
|
||||||
|
if (confirm("Are you sure you want to remove " + pluginName + " plugin?")) {
|
||||||
|
if (btn) {
|
||||||
|
$(btn).html('<i class="spinner loading icon"></i>');
|
||||||
|
$(btn).addClass('disabled');
|
||||||
|
}
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/plugins/store/uninstall',
|
||||||
|
type: 'POST',
|
||||||
|
data: { "pluginID": pluginId },
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
} else {
|
||||||
|
msgbox(pluginName + " uninstalled successfully", true);
|
||||||
|
initiatePluginList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,6 +43,10 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
<div class="ui warning message">
|
||||||
|
<div class="header">Experimental Feature</div>
|
||||||
|
<p>The Plugin Store is an experimental feature. Use it at your own risk.</p>
|
||||||
|
</div>
|
||||||
<div class="ui fluid search">
|
<div class="ui fluid search">
|
||||||
<div class="ui fluid icon input">
|
<div class="ui fluid icon input">
|
||||||
<input id="searchInput" class="prompt" type="text" placeholder="Search plugins">
|
<input id="searchInput" class="prompt" type="text" placeholder="Search plugins">
|
||||||
@ -53,7 +57,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
|
<button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
|
||||||
<div class="ui divider"></div>
|
<!-- <div class="ui divider"></div>
|
||||||
<div class="ui basic segment advanceoptions">
|
<div class="ui basic segment advanceoptions">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
@ -75,6 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div class="field" >
|
<div class="field" >
|
||||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||||
@ -126,7 +131,7 @@
|
|||||||
const name = item.querySelector('.header').textContent.toLowerCase();
|
const name = item.querySelector('.header').textContent.toLowerCase();
|
||||||
const description = item.querySelector('.description p').textContent.toLowerCase();
|
const description = item.querySelector('.description p').textContent.toLowerCase();
|
||||||
const author = item.querySelector('.meta span:nth-child(2)').textContent.toLowerCase();
|
const author = item.querySelector('.meta span:nth-child(2)').textContent.toLowerCase();
|
||||||
const id = item.querySelector('.extra button').getAttribute('onclick').match(/'(.*?)'/)[1].toLowerCase();
|
const id = item.getAttribute('plugin_id').toLowerCase();
|
||||||
|
|
||||||
if (name.includes(query) || description.includes(query) || author.includes(query) || id.includes(query)) {
|
if (name.includes(query) || description.includes(query) || author.includes(query) || id.includes(query)) {
|
||||||
item.style.display = '';
|
item.style.display = '';
|
||||||
@ -145,6 +150,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function forceResyncPlugins() {
|
function forceResyncPlugins() {
|
||||||
|
parent.msgbox("Updating plugin list...", true);
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: '/api/plugins/store/resync',
|
url: '/api/plugins/store/resync',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
@ -181,52 +188,58 @@
|
|||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span>Version: ${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch}</span>
|
<span>Version: ${plugin.PluginIntroSpect.version_major}.${plugin.PluginIntroSpect.version_minor}.${plugin.PluginIntroSpect.version_patch}</span>
|
||||||
<span>${plugin.PluginIntroSpect.author}</span>
|
<span>${plugin.PluginIntroSpect.author}</span>
|
||||||
<span><a href="${plugin.PluginIntroSpect.url}">Website</a></span>
|
<span><a href="${plugin.PluginIntroSpect.url}" target="_blank">Website</a></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<p>${plugin.PluginIntroSpect.description}</p>
|
<p>${plugin.PluginIntroSpect.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="action">
|
<div class="action">
|
||||||
${thisPluginIsInstalled
|
${thisPluginIsInstalled
|
||||||
? `<button class="ui basic circular red button" onclick="uninstallPlugin('${plugin.PluginIntroSpect.id}')"><i class="ui trash icon"></i> Remove</button>`
|
? `<button class="ui basic circular disabled button">Installed</button>`
|
||||||
: `<button class="ui basic circular button" onclick="installPlugin('${plugin.PluginIntroSpect.id}')"><i class="ui download icon"></i> Install</button>`}
|
: `<button class="ui basic circular button" onclick="installPlugin('${plugin.PluginIntroSpect.id}', this);",><i class="ui download icon"></i> Install</button>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
$('#pluginList').append(item);
|
$('#pluginList').append(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reapply search filter if there's a query in the search bar
|
||||||
|
const searchQuery = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
if (searchQuery.trim() !== '') {
|
||||||
|
searchPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Plugin Actions */
|
/* Plugin Actions */
|
||||||
function installPlugin(pluginId) {
|
function installPlugin(pluginId, btn=undefined) {
|
||||||
|
if (btn !== undefined) {
|
||||||
|
$(btn).addClass('loading').prop('disabled', true);
|
||||||
|
}
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: '/api/plugins/store/install',
|
url: '/api/plugins/store/install',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
data: { pluginId },
|
data: { "pluginID": pluginId },
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
|
if (btn !== undefined) {
|
||||||
|
$(btn).removeClass('loading').prop('disabled', false);
|
||||||
|
}
|
||||||
if (data.error != undefined) {
|
if (data.error != undefined) {
|
||||||
parent.msgbox(data.error, false);
|
parent.msgbox(data.error, false);
|
||||||
} else {
|
} else {
|
||||||
parent.msgbox("Plugin installed successfully", true);
|
parent.msgbox("Plugin installed successfully", true);
|
||||||
initStoreList();
|
initStoreList();
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninstallPlugin(pluginId) {
|
//Also reload the parent plugin list
|
||||||
$.cjax({
|
parent.initiatePluginList();
|
||||||
url: '/api/plugins/store/uninstall',
|
|
||||||
type: 'POST',
|
|
||||||
data: { pluginId },
|
|
||||||
success: function(data) {
|
|
||||||
if (data.error != undefined) {
|
|
||||||
parent.msgbox(data.error, false);
|
|
||||||
} else {
|
|
||||||
parent.msgbox("Plugin uninstalled successfully", true);
|
|
||||||
initStoreList();
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
if (btn !== undefined) {
|
||||||
|
$(btn).removeClass('loading').prop('disabled', false);
|
||||||
|
}
|
||||||
|
parent.msgbox("An error occurred while installing the plugin", false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user