node build fixed
This commit is contained in:
361
seanime-2.9.10/internal/plugin/ui/tray.go
Normal file
361
seanime-2.9.10/internal/plugin/ui/tray.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package plugin_ui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
type TrayManager struct {
|
||||
ctx *Context
|
||||
tray mo.Option[*Tray]
|
||||
lastUpdatedAt time.Time
|
||||
updateMutex sync.Mutex
|
||||
|
||||
componentManager *ComponentManager
|
||||
}
|
||||
|
||||
func NewTrayManager(ctx *Context) *TrayManager {
|
||||
return &TrayManager{
|
||||
ctx: ctx,
|
||||
tray: mo.None[*Tray](),
|
||||
componentManager: &ComponentManager{ctx: ctx},
|
||||
}
|
||||
}
|
||||
|
||||
// renderTrayScheduled renders the new component tree.
|
||||
// This function is unsafe because it is not thread-safe and should be scheduled.
|
||||
func (t *TrayManager) renderTrayScheduled() {
|
||||
t.updateMutex.Lock()
|
||||
defer t.updateMutex.Unlock()
|
||||
|
||||
tray, registered := t.tray.Get()
|
||||
if !registered {
|
||||
return
|
||||
}
|
||||
|
||||
if !tray.WithContent {
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limit updates
|
||||
//if time.Since(t.lastUpdatedAt) < time.Millisecond*200 {
|
||||
// return
|
||||
//}
|
||||
|
||||
t.lastUpdatedAt = time.Now()
|
||||
|
||||
t.ctx.scheduler.ScheduleAsync(func() error {
|
||||
// t.ctx.logger.Trace().Msg("plugin: Rendering tray")
|
||||
newComponents, err := t.componentManager.renderComponents(tray.renderFunc)
|
||||
if err != nil {
|
||||
t.ctx.logger.Error().Err(err).Msg("plugin: Failed to render tray")
|
||||
t.ctx.handleException(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// t.ctx.logger.Trace().Msg("plugin: Sending tray update to client")
|
||||
// Send the JSON value to the client
|
||||
t.ctx.SendEventToClient(ServerTrayUpdatedEvent, ServerTrayUpdatedEventPayload{
|
||||
Components: newComponents,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// sendIconToClient sends the tray icon to the client after it's been requested.
|
||||
func (t *TrayManager) sendIconToClient() {
|
||||
if tray, registered := t.tray.Get(); registered {
|
||||
t.ctx.SendEventToClient(ServerTrayIconEvent, ServerTrayIconEventPayload{
|
||||
ExtensionID: t.ctx.ext.ID,
|
||||
ExtensionName: t.ctx.ext.Name,
|
||||
IconURL: tray.IconURL,
|
||||
WithContent: tray.WithContent,
|
||||
TooltipText: tray.TooltipText,
|
||||
BadgeNumber: tray.BadgeNumber,
|
||||
BadgeIntent: tray.BadgeIntent,
|
||||
Width: tray.Width,
|
||||
MinHeight: tray.MinHeight,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Tray
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type Tray struct {
|
||||
// WithContent is used to determine if the tray has any content
|
||||
// If false, only the tray icon will be rendered and tray.render() will be ignored
|
||||
WithContent bool `json:"withContent"`
|
||||
|
||||
IconURL string `json:"iconUrl"`
|
||||
TooltipText string `json:"tooltipText"`
|
||||
BadgeNumber int `json:"badgeNumber"`
|
||||
BadgeIntent string `json:"badgeIntent"`
|
||||
Width string `json:"width,omitempty"`
|
||||
MinHeight string `json:"minHeight,omitempty"`
|
||||
|
||||
renderFunc func(goja.FunctionCall) goja.Value
|
||||
trayManager *TrayManager
|
||||
}
|
||||
|
||||
type Component struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Props map[string]interface{} `json:"props"`
|
||||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// jsNewTray
|
||||
//
|
||||
// Example:
|
||||
// const tray = ctx.newTray()
|
||||
func (t *TrayManager) jsNewTray(call goja.FunctionCall) goja.Value {
|
||||
tray := &Tray{
|
||||
renderFunc: nil,
|
||||
trayManager: t,
|
||||
WithContent: true,
|
||||
}
|
||||
|
||||
props := call.Arguments
|
||||
if len(props) > 0 {
|
||||
propsObj := props[0].Export().(map[string]interface{})
|
||||
if propsObj["withContent"] != nil {
|
||||
tray.WithContent, _ = propsObj["withContent"].(bool)
|
||||
}
|
||||
if propsObj["iconUrl"] != nil {
|
||||
tray.IconURL, _ = propsObj["iconUrl"].(string)
|
||||
}
|
||||
if propsObj["tooltipText"] != nil {
|
||||
tray.TooltipText, _ = propsObj["tooltipText"].(string)
|
||||
}
|
||||
if propsObj["width"] != nil {
|
||||
tray.Width, _ = propsObj["width"].(string)
|
||||
}
|
||||
if propsObj["minHeight"] != nil {
|
||||
tray.MinHeight, _ = propsObj["minHeight"].(string)
|
||||
}
|
||||
}
|
||||
|
||||
t.tray = mo.Some(tray)
|
||||
|
||||
// Create a new tray object
|
||||
trayObj := t.ctx.vm.NewObject()
|
||||
_ = trayObj.Set("render", tray.jsRender)
|
||||
_ = trayObj.Set("update", tray.jsUpdate)
|
||||
_ = trayObj.Set("onOpen", tray.jsOnOpen)
|
||||
_ = trayObj.Set("onClose", tray.jsOnClose)
|
||||
_ = trayObj.Set("onClick", tray.jsOnClick)
|
||||
_ = trayObj.Set("open", tray.jsOpen)
|
||||
_ = trayObj.Set("close", tray.jsClose)
|
||||
_ = trayObj.Set("updateBadge", tray.jsUpdateBadge)
|
||||
|
||||
// Register components
|
||||
_ = trayObj.Set("div", t.componentManager.jsDiv)
|
||||
_ = trayObj.Set("flex", t.componentManager.jsFlex)
|
||||
_ = trayObj.Set("stack", t.componentManager.jsStack)
|
||||
_ = trayObj.Set("text", t.componentManager.jsText)
|
||||
_ = trayObj.Set("button", t.componentManager.jsButton)
|
||||
_ = trayObj.Set("anchor", t.componentManager.jsAnchor)
|
||||
_ = trayObj.Set("input", t.componentManager.jsInput)
|
||||
_ = trayObj.Set("radioGroup", t.componentManager.jsRadioGroup)
|
||||
_ = trayObj.Set("switch", t.componentManager.jsSwitch)
|
||||
_ = trayObj.Set("checkbox", t.componentManager.jsCheckbox)
|
||||
_ = trayObj.Set("select", t.componentManager.jsSelect)
|
||||
|
||||
return trayObj
|
||||
}
|
||||
|
||||
/////
|
||||
|
||||
// jsRender registers a function to be called when the tray is rendered/updated
|
||||
//
|
||||
// Example:
|
||||
// tray.render(() => flex)
|
||||
func (t *Tray) jsRender(call goja.FunctionCall) goja.Value {
|
||||
|
||||
funcRes, ok := call.Argument(0).Export().(func(goja.FunctionCall) goja.Value)
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("render requires a function")
|
||||
}
|
||||
|
||||
// Set the render function
|
||||
t.renderFunc = funcRes
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsUpdate schedules a re-render on the client
|
||||
//
|
||||
// Example:
|
||||
// tray.update()
|
||||
func (t *Tray) jsUpdate(call goja.FunctionCall) goja.Value {
|
||||
// Update the context's lastUIUpdateAt to prevent duplicate updates
|
||||
t.trayManager.ctx.uiUpdateMu.Lock()
|
||||
t.trayManager.ctx.lastUIUpdateAt = time.Now()
|
||||
t.trayManager.ctx.uiUpdateMu.Unlock()
|
||||
|
||||
t.trayManager.renderTrayScheduled()
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsOpen
|
||||
//
|
||||
// Example:
|
||||
// tray.open()
|
||||
func (t *Tray) jsOpen(call goja.FunctionCall) goja.Value {
|
||||
t.trayManager.ctx.SendEventToClient(ServerTrayOpenEvent, ServerTrayOpenEventPayload{
|
||||
ExtensionID: t.trayManager.ctx.ext.ID,
|
||||
})
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsClose
|
||||
//
|
||||
// Example:
|
||||
// tray.close()
|
||||
func (t *Tray) jsClose(call goja.FunctionCall) goja.Value {
|
||||
t.trayManager.ctx.SendEventToClient(ServerTrayCloseEvent, ServerTrayCloseEventPayload{
|
||||
ExtensionID: t.trayManager.ctx.ext.ID,
|
||||
})
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsUpdateBadge
|
||||
//
|
||||
// Example:
|
||||
// tray.updateBadge({ number: 1, intent: "success" })
|
||||
func (t *Tray) jsUpdateBadge(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
t.trayManager.ctx.handleTypeError("updateBadge requires a callback function")
|
||||
}
|
||||
|
||||
propsObj, ok := call.Argument(0).Export().(map[string]interface{})
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("updateBadge requires a callback function")
|
||||
}
|
||||
|
||||
number, ok := propsObj["number"].(int64)
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("updateBadge: number must be an integer")
|
||||
}
|
||||
|
||||
intent, ok := propsObj["intent"].(string)
|
||||
if !ok {
|
||||
intent = "info"
|
||||
}
|
||||
|
||||
t.BadgeNumber = int(number)
|
||||
t.BadgeIntent = intent
|
||||
|
||||
t.trayManager.ctx.SendEventToClient(ServerTrayBadgeUpdatedEvent, ServerTrayBadgeUpdatedEventPayload{
|
||||
BadgeNumber: t.BadgeNumber,
|
||||
BadgeIntent: t.BadgeIntent,
|
||||
})
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsOnOpen
|
||||
//
|
||||
// Example:
|
||||
// tray.onOpen(() => {
|
||||
// console.log("tray opened by the user")
|
||||
// })
|
||||
func (t *Tray) jsOnOpen(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
t.trayManager.ctx.handleTypeError("onOpen requires a callback function")
|
||||
}
|
||||
|
||||
callback, ok := goja.AssertFunction(call.Argument(0))
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("onOpen requires a callback function")
|
||||
}
|
||||
|
||||
eventListener := t.trayManager.ctx.RegisterEventListener(ClientTrayOpenedEvent)
|
||||
payload := ClientTrayOpenedEventPayload{}
|
||||
|
||||
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
||||
if event.ParsePayloadAs(ClientTrayOpenedEvent, &payload) {
|
||||
t.trayManager.ctx.scheduler.ScheduleAsync(func() error {
|
||||
_, err := callback(goja.Undefined(), t.trayManager.ctx.vm.ToValue(map[string]interface{}{}))
|
||||
if err != nil {
|
||||
t.trayManager.ctx.logger.Error().Err(err).Msg("plugin: Error running tray open callback")
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsOnClick
|
||||
//
|
||||
// Example:
|
||||
// tray.onClick(() => {
|
||||
// console.log("tray clicked by the user")
|
||||
// })
|
||||
func (t *Tray) jsOnClick(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
t.trayManager.ctx.handleTypeError("onClick requires a callback function")
|
||||
}
|
||||
|
||||
callback, ok := goja.AssertFunction(call.Argument(0))
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("onClick requires a callback function")
|
||||
}
|
||||
|
||||
eventListener := t.trayManager.ctx.RegisterEventListener(ClientTrayClickedEvent)
|
||||
payload := ClientTrayClickedEventPayload{}
|
||||
|
||||
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
||||
if event.ParsePayloadAs(ClientTrayClickedEvent, &payload) {
|
||||
t.trayManager.ctx.scheduler.ScheduleAsync(func() error {
|
||||
_, err := callback(goja.Undefined(), t.trayManager.ctx.vm.ToValue(map[string]interface{}{}))
|
||||
if err != nil {
|
||||
t.trayManager.ctx.logger.Error().Err(err).Msg("plugin: Error running tray click callback")
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
|
||||
// jsOnClose
|
||||
//
|
||||
// Example:
|
||||
// tray.onClose(() => {
|
||||
// console.log("tray closed by the user")
|
||||
// })
|
||||
func (t *Tray) jsOnClose(call goja.FunctionCall) goja.Value {
|
||||
if len(call.Arguments) < 1 {
|
||||
t.trayManager.ctx.handleTypeError("onClose requires a callback function")
|
||||
}
|
||||
|
||||
callback, ok := goja.AssertFunction(call.Argument(0))
|
||||
if !ok {
|
||||
t.trayManager.ctx.handleTypeError("onClose requires a callback function")
|
||||
}
|
||||
|
||||
eventListener := t.trayManager.ctx.RegisterEventListener(ClientTrayClosedEvent)
|
||||
payload := ClientTrayClosedEventPayload{}
|
||||
|
||||
eventListener.SetCallback(func(event *ClientPluginEvent) {
|
||||
if event.ParsePayloadAs(ClientTrayClosedEvent, &payload) {
|
||||
t.trayManager.ctx.scheduler.ScheduleAsync(func() error {
|
||||
_, err := callback(goja.Undefined(), t.trayManager.ctx.vm.ToValue(map[string]interface{}{}))
|
||||
if err != nil {
|
||||
t.trayManager.ctx.logger.Error().Err(err).Msg("plugin: Error running tray close callback")
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return goja.Undefined()
|
||||
}
|
||||
Reference in New Issue
Block a user