- Added dev mode plugin auto-reload
- Optimized struct in plugin manager options
This commit is contained in:
Toby Chui 2025-05-11 14:02:07 +08:00
parent b9c609e413
commit 877692695e
9 changed files with 416 additions and 51 deletions

View File

@ -238,6 +238,10 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
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/install", pluginManager.HandleInstallPlugin)
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin) authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
// Developer options
authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload)
authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval)
} }
// 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

View File

@ -0,0 +1,214 @@
package plugins
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strconv"
"time"
"imuslab.com/zoraxy/mod/utils"
)
// StartHotReloadTicker starts the hot reload ticker
func (m *Manager) StartHotReloadTicker() error {
if m.pluginReloadTicker != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already started", nil)
return errors.New("hot reload ticker already started")
}
m.pluginReloadTicker = time.NewTicker(time.Duration(m.Options.HotReloadInterval) * time.Second)
m.pluginReloadStop = make(chan bool)
go func() {
for {
select {
case <-m.pluginReloadTicker.C:
err := m.UpdatePluginHashList(false)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to update plugin hash list", err)
}
case <-m.pluginReloadStop:
return
}
}
}()
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker started", nil)
return nil
}
// StopHotReloadTicker stops the hot reload ticker
func (m *Manager) StopHotReloadTicker() error {
if m.pluginReloadTicker != nil {
m.pluginReloadStop <- true
m.pluginReloadTicker.Stop()
m.pluginReloadTicker = nil
m.pluginReloadStop = nil
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker stopped", nil)
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already stopped", nil)
}
return nil
}
func (m *Manager) InitPluginHashList() error {
return m.UpdatePluginHashList(true)
}
// Update the plugin hash list and if there are change, reload the plugin
func (m *Manager) UpdatePluginHashList(noReload bool) error {
for pluginId, plugin := range m.LoadedPlugins {
//Get the plugin Entry point
pluginEntryPoint, err := m.GetPluginEntryPoint(plugin.RootDir)
if err != nil {
//Unable to get the entry point of the plugin
return err
}
file, err := os.Open(pluginEntryPoint)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to open plugin entry point: "+pluginEntryPoint, err)
return err
}
defer file.Close()
//Calculate the hash of the file
hasher := sha256.New()
if _, err := file.Seek(0, 0); err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to seek plugin entry point: "+pluginEntryPoint, err)
return err
}
if _, err := io.Copy(hasher, file); err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to copy plugin entry point: "+pluginEntryPoint, err)
return err
}
hash := hex.EncodeToString(hasher.Sum(nil))
m.pluginCheckMutex.Lock()
if m.PluginHash[pluginId] != hash {
m.PluginHash[pluginId] = hash
m.pluginCheckMutex.Unlock()
if !noReload {
//Plugin file changed, reload the plugin
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin file changed, reloading plugin: "+pluginId, nil)
err := m.HotReloadPlugin(pluginId)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to reload plugin: "+pluginId, err)
return err
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin reloaded: "+pluginId, nil)
}
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin hash generated for: "+pluginId, nil)
}
} else {
m.pluginCheckMutex.Unlock()
}
}
return nil
}
// Reload the plugin from file system
func (m *Manager) HotReloadPlugin(pluginId string) error {
//Check if the plugin is currently running
thisPlugin, err := m.GetPluginByID(pluginId)
if err != nil {
return err
}
if thisPlugin.IsRunning() {
err = m.StopPlugin(pluginId)
if err != nil {
return err
}
}
//Remove the plugin from the loaded plugins list
m.loadedPluginsMutex.Lock()
if _, ok := m.LoadedPlugins[pluginId]; ok {
delete(m.LoadedPlugins, pluginId)
} else {
m.loadedPluginsMutex.Unlock()
return nil
}
m.loadedPluginsMutex.Unlock()
//Reload the plugin from disk, it should reload the plugin from latest version
m.ReloadPluginFromDisk()
return nil
}
/*
Request handlers for developer options
*/
func (m *Manager) HandleEnableHotReload(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current status of hot reload
js, _ := json.Marshal(m.Options.EnableHotReload)
utils.SendJSONResponse(w, string(js))
return
}
enabled, err := utils.PostBool(r, "enabled")
if err != nil {
utils.SendErrorResponse(w, "enabled not found")
return
}
m.Options.EnableHotReload = enabled
if enabled {
//Start the hot reload ticker
err := m.StartHotReloadTicker()
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
utils.SendErrorResponse(w, "Failed to start hot reload ticker")
return
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload enabled", nil)
} else {
//Stop the hot reload ticker
err := m.StopHotReloadTicker()
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to stop hot reload ticker", err)
utils.SendErrorResponse(w, "Failed to stop hot reload ticker")
return
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload disabled", nil)
}
utils.SendOK(w)
}
func (m *Manager) HandleSetHotReloadInterval(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current status of hot reload
js, _ := json.Marshal(m.Options.HotReloadInterval)
utils.SendJSONResponse(w, string(js))
return
}
interval, err := utils.PostInt(r, "interval")
if err != nil {
utils.SendErrorResponse(w, "interval not found")
return
}
if interval < 1 {
utils.SendErrorResponse(w, "interval must be at least 1 second")
return
}
m.Options.HotReloadInterval = interval
//Restart the hot reload ticker
if m.pluginReloadTicker != nil {
m.StopHotReloadTicker()
time.Sleep(1 * time.Second)
//Start the hot reload ticker again
m.StartHotReloadTicker()
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload interval set to "+strconv.Itoa(interval)+" sec", nil)
utils.SendOK(w)
}

View File

@ -11,11 +11,11 @@ import (
// ListPluginGroups returns a map of plugin groups // ListPluginGroups returns a map of plugin groups
func (m *Manager) ListPluginGroups() map[string][]string { func (m *Manager) ListPluginGroups() map[string][]string {
pluginGroup := map[string][]string{} pluginGroup := map[string][]string{}
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
for k, v := range m.Options.PluginGroups { for k, v := range m.Options.PluginGroups {
pluginGroup[k] = append([]string{}, v...) pluginGroup[k] = append([]string{}, v...)
} }
m.Options.pluginGroupsMutex.RUnlock() m.pluginGroupsMutex.RUnlock()
return pluginGroup return pluginGroup
} }
@ -32,26 +32,26 @@ func (m *Manager) AddPluginToGroup(tag, pluginID string) error {
return errors.New("plugin is not a router type plugin") return errors.New("plugin is not a router type plugin")
} }
m.Options.pluginGroupsMutex.Lock() m.pluginGroupsMutex.Lock()
//Check if the tag exists //Check if the tag exists
_, ok = m.Options.PluginGroups[tag] _, ok = m.Options.PluginGroups[tag]
if !ok { if !ok {
m.Options.PluginGroups[tag] = []string{pluginID} m.Options.PluginGroups[tag] = []string{pluginID}
m.Options.pluginGroupsMutex.Unlock() m.pluginGroupsMutex.Unlock()
return nil return nil
} }
//Add the plugin to the group //Add the plugin to the group
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID) m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
m.Options.pluginGroupsMutex.Unlock() m.pluginGroupsMutex.Unlock()
return nil return nil
} }
// RemovePluginFromGroup removes a plugin from a group // RemovePluginFromGroup removes a plugin from a group
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error { func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
m.Options.pluginGroupsMutex.Lock() m.pluginGroupsMutex.Lock()
defer m.Options.pluginGroupsMutex.Unlock() defer m.pluginGroupsMutex.Unlock()
//Check if the tag exists //Check if the tag exists
_, ok := m.Options.PluginGroups[tag] _, ok := m.Options.PluginGroups[tag]
if !ok { if !ok {
@ -72,8 +72,8 @@ func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
// RemovePluginGroup removes a plugin group // RemovePluginGroup removes a plugin group
func (m *Manager) RemovePluginGroup(tag string) error { func (m *Manager) RemovePluginGroup(tag string) error {
m.Options.pluginGroupsMutex.Lock() m.pluginGroupsMutex.Lock()
defer m.Options.pluginGroupsMutex.Unlock() defer m.pluginGroupsMutex.Unlock()
_, ok := m.Options.PluginGroups[tag] _, ok := m.Options.PluginGroups[tag]
if !ok { if !ok {
return errors.New("tag not found") return errors.New("tag not found")
@ -84,12 +84,12 @@ func (m *Manager) RemovePluginGroup(tag string) error {
// SavePluginGroupsFromFile loads plugin groups from a file // SavePluginGroupsFromFile loads plugin groups from a file
func (m *Manager) SavePluginGroupsToFile() error { func (m *Manager) SavePluginGroupsToFile() error {
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
pluginGroupsCopy := make(map[string][]string) pluginGroupsCopy := make(map[string][]string)
for k, v := range m.Options.PluginGroups { for k, v := range m.Options.PluginGroups {
pluginGroupsCopy[k] = append([]string{}, v...) pluginGroupsCopy[k] = append([]string{}, v...)
} }
m.Options.pluginGroupsMutex.RUnlock() m.pluginGroupsMutex.RUnlock()
//Write to file //Write to file
js, _ := json.Marshal(pluginGroupsCopy) js, _ := json.Marshal(pluginGroupsCopy)

View File

@ -47,15 +47,26 @@ func NewPluginManager(options *ManagerOptions) *Manager {
//Create database table //Create database table
options.Database.NewTable("plugins") options.Database.NewTable("plugins")
return &Manager{ thisManager := &Manager{
LoadedPlugins: make(map[string]*Plugin), LoadedPlugins: make(map[string]*Plugin),
tagPluginMap: sync.Map{}, tagPluginMap: sync.Map{},
tagPluginListMutex: sync.RWMutex{}, tagPluginListMutex: sync.RWMutex{},
tagPluginList: make(map[string][]*Plugin), tagPluginList: make(map[string][]*Plugin),
Options: options, Options: options,
PluginHash: make(map[string]string),
/* Internal */ /* Internal */
loadedPluginsMutex: sync.RWMutex{}, loadedPluginsMutex: sync.RWMutex{},
} }
//Check if hot reload is enabled
if options.EnableHotReload {
err := thisManager.StartHotReloadTicker()
if err != nil {
options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
}
}
return thisManager
} }
// Reload all plugins from disk // Reload all plugins from disk
@ -104,11 +115,16 @@ func (m *Manager) ReloadPluginFromDisk() {
m.loadedPluginsMutex.Lock() m.loadedPluginsMutex.Lock()
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
m.loadedPluginsMutex.Unlock() m.loadedPluginsMutex.Unlock()
m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil) versionNumber := strconv.Itoa(thisPlugin.Spec.VersionMajor) + "." + strconv.Itoa(thisPlugin.Spec.VersionMinor) + "." + strconv.Itoa(thisPlugin.Spec.VersionPatch)
//Check if the plugin is enabled
m.Log("Found plugin: "+thisPlugin.Spec.Name+" (v"+versionNumber+")", nil)
// The default state of the plugin is disabled, so no need to start it // The default state of the plugin is disabled, so no need to start it
} }
} }
//Generate a hash list for plugins
m.InitPluginHashList()
} }
// LoadPluginsFromDisk loads all plugins from the plugin directory // LoadPluginsFromDisk loads all plugins from the plugin directory
@ -156,6 +172,8 @@ func (m *Manager) LoadPluginsFromDisk() error {
//Generate the static forwarder radix tree //Generate the static forwarder radix tree
m.UpdateTagsToPluginMaps() m.UpdateTagsToPluginMaps()
//Generate a hash list for plugins
m.InitPluginHashList()
return nil return nil
} }

View File

@ -17,8 +17,8 @@ import (
// This will only load the plugin tags to option.PluginGroups map // This will only load the plugin tags to option.PluginGroups map
// to push the changes to runtime, call UpdateTagsToPluginMaps() // to push the changes to runtime, call UpdateTagsToPluginMaps()
func (m *Manager) LoadPluginGroupsFromConfig() error { func (m *Manager) LoadPluginGroupsFromConfig() error {
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock() defer m.pluginGroupsMutex.RUnlock()
//Read the config file //Read the config file
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig) rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
@ -39,8 +39,8 @@ func (m *Manager) LoadPluginGroupsFromConfig() error {
// AddPluginToTag adds a plugin to a tag // AddPluginToTag adds a plugin to a tag
func (m *Manager) AddPluginToTag(tag string, pluginID string) error { func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock() defer m.pluginGroupsMutex.RUnlock()
//Check if the plugin exists //Check if the plugin exists
_, err := m.GetPluginByID(pluginID) _, err := m.GetPluginByID(pluginID)
@ -66,8 +66,8 @@ func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
// RemovePluginFromTag removes a plugin from a tag // RemovePluginFromTag removes a plugin from a tag
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error { func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
// Check if the plugin exists in Options.PluginGroups // Check if the plugin exists in Options.PluginGroups
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock() defer m.pluginGroupsMutex.RUnlock()
pluginList, ok := m.Options.PluginGroups[tag] pluginList, ok := m.Options.PluginGroups[tag]
if !ok { if !ok {
return nil return nil
@ -91,8 +91,8 @@ func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
// savePluginTagMap saves the plugin tag map to the config file // savePluginTagMap saves the plugin tag map to the config file
func (m *Manager) savePluginTagMap() error { func (m *Manager) savePluginTagMap() error {
m.Options.pluginGroupsMutex.RLock() m.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock() defer m.pluginGroupsMutex.RUnlock()
js, _ := json.Marshal(m.Options.PluginGroups) js, _ := json.Marshal(m.Options.PluginGroups)
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644) return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)

View File

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"os/exec" "os/exec"
"sync" "sync"
"time"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@ -45,8 +46,9 @@ type ManagerOptions struct {
Database *database.Database `json:"-"` Database *database.Database `json:"-"`
Logger *logger.Logger `json:"-"` Logger *logger.Logger `json:"-"`
/* Internal */ /* Development */
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
HotReloadInterval int //The interval for checking the plugin file change, in seconds
} }
type Manager struct { type Manager struct {
@ -56,6 +58,12 @@ type Manager struct {
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
Options *ManagerOptions Options *ManagerOptions
PluginHash map[string]string //The hash of the plugin file, used to check if the plugin file is changed
/* Internal */ /* Internal */
loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
pluginCheckMutex sync.RWMutex //Mutex for the plugin hash
pluginReloadTicker *time.Ticker //Ticker for the plugin reload
pluginReloadStop chan bool //Channel to stop the plugin reload ticker
} }

View File

@ -307,21 +307,26 @@ func startupSequence() {
pluginFolder := *path_plugin pluginFolder := *path_plugin
pluginFolder = strings.TrimSuffix(pluginFolder, "/") pluginFolder = strings.TrimSuffix(pluginFolder, "/")
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{ pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
PluginDir: pluginFolder, PluginDir: pluginFolder,
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
ZoraxyVersion: SYSTEM_VERSION,
ZoraxyUUID: nodeUUID,
DevelopmentBuild: *development_build,
},
PluginStoreURLs: []string{
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
},
Database: sysdb, Database: sysdb,
Logger: SystemWideLogger, Logger: SystemWideLogger,
PluginGroupsConfig: CONF_PLUGIN_GROUPS, PluginGroupsConfig: CONF_PLUGIN_GROUPS,
CSRFTokenGen: func(r *http.Request) string { CSRFTokenGen: func(r *http.Request) string {
return csrf.Token(r) return csrf.Token(r)
}, },
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
ZoraxyVersion: SYSTEM_VERSION,
ZoraxyUUID: nodeUUID,
DevelopmentBuild: *development_build,
},
/* Plugin Store URLs */
PluginStoreURLs: []string{
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
//TO BE ADDED
},
/* Developer Options */
EnableHotReload: *development_build, //Default to true if development build
HotReloadInterval: 5, //seconds
}) })
//Sync latest plugin list from the plugin store //Sync latest plugin list from the plugin store

View File

@ -185,6 +185,33 @@
</tbody> </tbody>
</table> </table>
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Developer Settings
</div>
<div class="content ui form">
<div class="ui inverted message" style="margin-top: 0.6em;">
<div class="header">Developer Only</div>
<p>These functions are intended for developers only. Enabling them may add latency to plugin loading & routing. Proceed with caution.<br>
<b>Tips: You can start zoraxy with -dev=true to enable auto-reload when start</b></p>
</div>
<div id="enablePluginAutoReload" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input id="enable_plugin_auto_reload" type="checkbox">
<label>Enable Plugin Auto Reload<br>
<small>Automatic reload plugin when the plugin binary changed</small></label>
</div>
<br><br>
<div class="field" style="max-width: 50%;margin-bottom: 0px;">
<label>Check Interval</label>
<input type="number" id="autoreload-interval" placeholder="Check Interval" min="1" max="60" step="1" value="1">
</div>
<small>Specify the interval (in seconds) for checking plugin changes. <br>Minimum is 1 second, maximum is 60 seconds.</small>
</div>
</div>
</div>
<br> <br>
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button> <button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
</div> </div>
@ -592,6 +619,95 @@ function uninstallPlugin(pluginId, pluginName, btn=undefined) {
} }
} }
/* Developer Settings */
function initDeveloperSettings() {
// Fetch the auto reload status
$.get('/api/plugins/developer/enableAutoReload', function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
return;
}
// Set the checkbox for Plugin Auto Reload
if (data == true) {
$("#enablePluginAutoReload").checkbox('set checked');
} else {
$("#enablePluginAutoReload").checkbox('set unchecked');
}
// Fetch the auto reload interval
$.get('/api/plugins/developer/setAutoReloadInterval', function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
return;
}
// Set the input value for Auto Reload Interval
if (data) {
$("#autoreload-interval").val(data);
}
bindEventsToDeveloperSettings();
});
});
}
function bindEventsToDeveloperSettings(){
$("#enablePluginAutoReload").checkbox({
onChecked: function() {
$.cjax({
url: '/api/plugins/developer/enableAutoReload',
type: 'POST',
data: { "enabled": true },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Plugin Auto Reload enabled", true);
}
}
});
},
onUnchecked: function() {
$.cjax({
url: '/api/plugins/developer/enableAutoReload',
type: 'POST',
data: { "enabled": false },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Plugin Auto Reload disabled", true);
}
}
});
}
});
$("#autoreload-interval").on("change", function() {
const interval = $(this).val();
if (interval < 1 || interval > 60) {
msgbox("Interval must be between 1 and 60 seconds", false);
return;
}
$.cjax({
url: '/api/plugins/developer/setAutoReloadInterval',
type: 'POST',
data: { "interval": interval },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Auto Reload Interval updated to " + interval + " seconds", true);
}
}
});
});
}
initDeveloperSettings();
</script> </script>

View File

@ -58,28 +58,28 @@
</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">
<i class="dropdown icon"></i> <i class="dropdown icon"></i>
Advance Settings Advance Settings
</div> </div>
<div class="content"> <div class="content">
<p>Plugin Store URLs</p> <p>Plugin Store URLs</p>
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<textarea id="pluginStoreURLs" rows="5"></textarea> <textarea id="pluginStoreURLs" rows="5"></textarea>
<label>Enter plugin store URLs, separating each URL with a new line</label> <label>Enter plugin store URLs, separating each URL with a new line</label>
</div> </div>
<button class="ui basic button" onclick="savePluginStoreURLs()"> <button class="ui basic button" onclick="savePluginStoreURLs()">
<i class="ui green save icon"></i>Save <i class="ui green save icon"></i>Save
</button> </button>
</div>
</div> </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>