node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,683 @@
package plugin_ui
import (
"fmt"
"seanime/internal/util/result"
"github.com/dop251/goja"
"github.com/goccy/go-json"
"github.com/google/uuid"
)
const (
MaxActionsPerType = 3 // A plugin can only at most X actions of a certain type
)
// ActionManager
//
// Actions are buttons, dropdown items, and context menu items that are displayed in certain places in the UI.
// They are defined in the plugin code and are used to trigger events.
//
// The ActionManager is responsible for registering, rendering, and handling events for actions.
type ActionManager struct {
ctx *Context
animePageButtons *result.Map[string, *AnimePageButton]
animePageDropdownItems *result.Map[string, *AnimePageDropdownMenuItem]
animeLibraryDropdownItems *result.Map[string, *AnimeLibraryDropdownMenuItem]
mangaPageButtons *result.Map[string, *MangaPageButton]
mediaCardContextMenuItems *result.Map[string, *MediaCardContextMenuItem]
episodeCardContextMenuItems *result.Map[string, *EpisodeCardContextMenuItem]
episodeGridItemMenuItems *result.Map[string, *EpisodeGridItemMenuItem]
}
type BaseActionProps struct {
ID string `json:"id"`
Label string `json:"label"`
Style map[string]string `json:"style,omitempty"`
}
// Base action struct that all action types embed
type BaseAction struct {
BaseActionProps
}
// GetProps returns the base action properties
func (a *BaseAction) GetProps() BaseActionProps {
return a.BaseActionProps
}
// SetProps sets the base action properties
func (a *BaseAction) SetProps(props BaseActionProps) {
a.BaseActionProps = props
}
// UnmountAll unmounts all actions
// It should be called when the plugin is unloaded
func (a *ActionManager) UnmountAll() {
if a.animePageButtons.ClearN() > 0 {
a.renderAnimePageButtons()
}
if a.animePageDropdownItems.ClearN() > 0 {
a.renderAnimePageDropdownItems()
}
if a.animeLibraryDropdownItems.ClearN() > 0 {
a.renderAnimeLibraryDropdownItems()
}
if a.mangaPageButtons.ClearN() > 0 {
a.renderMangaPageButtons()
}
if a.mediaCardContextMenuItems.ClearN() > 0 {
a.renderMediaCardContextMenuItems()
}
if a.episodeCardContextMenuItems.ClearN() > 0 {
a.renderEpisodeCardContextMenuItems()
}
if a.episodeGridItemMenuItems.ClearN() > 0 {
a.renderEpisodeGridItemMenuItems()
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type AnimePageButton struct {
BaseAction
Intent string `json:"intent,omitempty"`
}
func (a *AnimePageButton) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
_ = obj.Set("setIntent", func(intent string) {
a.Intent = intent
})
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type EpisodeCardContextMenuItem struct {
BaseAction
}
func (a *EpisodeCardContextMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type EpisodeGridItemMenuItem struct {
BaseAction
Type string `json:"type"`
}
func (a *EpisodeGridItemMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type MangaPageButton struct {
BaseAction
Intent string `json:"intent,omitempty"`
}
func (a *MangaPageButton) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
_ = obj.Set("setIntent", func(intent string) {
a.Intent = intent
})
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type AnimePageDropdownMenuItem struct {
BaseAction
}
func (a *AnimePageDropdownMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type AnimeLibraryDropdownMenuItem struct {
BaseAction
}
func (a *AnimeLibraryDropdownMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
type MediaCardContextMenuItemFor string
const (
MediaCardContextMenuItemForAnime MediaCardContextMenuItemFor = "anime"
MediaCardContextMenuItemForManga MediaCardContextMenuItemFor = "manga"
MediaCardContextMenuItemForBoth MediaCardContextMenuItemFor = "both"
)
type MediaCardContextMenuItem struct {
BaseAction
For MediaCardContextMenuItemFor `json:"for"` // anime, manga, both
}
func (a *MediaCardContextMenuItem) CreateObject(actionManager *ActionManager) *goja.Object {
obj := actionManager.ctx.vm.NewObject()
actionManager.bindSharedToObject(obj, a)
_ = obj.Set("setFor", func(_for MediaCardContextMenuItemFor) {
a.For = _for
})
return obj
}
// ///////////////////////////////////////////////////////////////////////////////////////////////////////////
func NewActionManager(ctx *Context) *ActionManager {
return &ActionManager{
ctx: ctx,
animePageButtons: result.NewResultMap[string, *AnimePageButton](),
animeLibraryDropdownItems: result.NewResultMap[string, *AnimeLibraryDropdownMenuItem](),
animePageDropdownItems: result.NewResultMap[string, *AnimePageDropdownMenuItem](),
mangaPageButtons: result.NewResultMap[string, *MangaPageButton](),
mediaCardContextMenuItems: result.NewResultMap[string, *MediaCardContextMenuItem](),
episodeCardContextMenuItems: result.NewResultMap[string, *EpisodeCardContextMenuItem](),
episodeGridItemMenuItems: result.NewResultMap[string, *EpisodeGridItemMenuItem](),
}
}
// renderAnimePageButtons is called when the client requests the buttons to display on the anime page.
func (a *ActionManager) renderAnimePageButtons() {
buttons := make([]*AnimePageButton, 0)
a.animePageButtons.Range(func(key string, value *AnimePageButton) bool {
buttons = append(buttons, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderAnimePageButtonsEvent, ServerActionRenderAnimePageButtonsEventPayload{
Buttons: buttons,
})
}
func (a *ActionManager) renderAnimePageDropdownItems() {
items := make([]*AnimePageDropdownMenuItem, 0)
a.animePageDropdownItems.Range(func(key string, value *AnimePageDropdownMenuItem) bool {
items = append(items, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderAnimePageDropdownItemsEvent, ServerActionRenderAnimePageDropdownItemsEventPayload{
Items: items,
})
}
func (a *ActionManager) renderAnimeLibraryDropdownItems() {
items := make([]*AnimeLibraryDropdownMenuItem, 0)
a.animeLibraryDropdownItems.Range(func(key string, value *AnimeLibraryDropdownMenuItem) bool {
items = append(items, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderAnimeLibraryDropdownItemsEvent, ServerActionRenderAnimeLibraryDropdownItemsEventPayload{
Items: items,
})
}
func (a *ActionManager) renderMangaPageButtons() {
buttons := make([]*MangaPageButton, 0)
a.mangaPageButtons.Range(func(key string, value *MangaPageButton) bool {
buttons = append(buttons, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderMangaPageButtonsEvent, ServerActionRenderMangaPageButtonsEventPayload{
Buttons: buttons,
})
}
func (a *ActionManager) renderMediaCardContextMenuItems() {
items := make([]*MediaCardContextMenuItem, 0)
a.mediaCardContextMenuItems.Range(func(key string, value *MediaCardContextMenuItem) bool {
items = append(items, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderMediaCardContextMenuItemsEvent, ServerActionRenderMediaCardContextMenuItemsEventPayload{
Items: items,
})
}
func (a *ActionManager) renderEpisodeCardContextMenuItems() {
items := make([]*EpisodeCardContextMenuItem, 0)
a.episodeCardContextMenuItems.Range(func(key string, value *EpisodeCardContextMenuItem) bool {
items = append(items, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderEpisodeCardContextMenuItemsEvent, ServerActionRenderEpisodeCardContextMenuItemsEventPayload{
Items: items,
})
}
func (a *ActionManager) renderEpisodeGridItemMenuItems() {
items := make([]*EpisodeGridItemMenuItem, 0)
a.episodeGridItemMenuItems.Range(func(key string, value *EpisodeGridItemMenuItem) bool {
items = append(items, value)
return true
})
a.ctx.SendEventToClient(ServerActionRenderEpisodeGridItemMenuItemsEvent, ServerActionRenderEpisodeGridItemMenuItemsEventPayload{
Items: items,
})
}
// bind binds 'action' to the ctx object
//
// Example:
// ctx.action.newAnimePageButton(...)
func (a *ActionManager) bind(ctxObj *goja.Object) {
actionObj := a.ctx.vm.NewObject()
_ = actionObj.Set("newAnimePageButton", a.jsNewAnimePageButton)
_ = actionObj.Set("newAnimePageDropdownItem", a.jsNewAnimePageDropdownItem)
_ = actionObj.Set("newAnimeLibraryDropdownItem", a.jsNewAnimeLibraryDropdownItem)
_ = actionObj.Set("newMediaCardContextMenuItem", a.jsNewMediaCardContextMenuItem)
_ = actionObj.Set("newMangaPageButton", a.jsNewMangaPageButton)
_ = actionObj.Set("newEpisodeCardContextMenuItem", a.jsNewEpisodeCardContextMenuItem)
_ = actionObj.Set("newEpisodeGridItemMenuItem", a.jsNewEpisodeGridItemMenuItem)
_ = ctxObj.Set("action", actionObj)
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Actions
////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewEpisodeCardContextMenuItem
//
// Example:
// const downloadButton = ctx.newEpisodeCardContextMenuItem({
// label: "Download",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewEpisodeCardContextMenuItem(call goja.FunctionCall) goja.Value {
// Create a new action
action := &EpisodeCardContextMenuItem{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewEpisodeGridItemMenuItem
//
// Example:
// const downloadButton = ctx.newEpisodeGridItemContextMenuItem({
// label: "Download",
// onClick: "download-button-clicked",
// type: "library",
// })
func (a *ActionManager) jsNewEpisodeGridItemMenuItem(call goja.FunctionCall) goja.Value {
// Create a new action
action := &EpisodeGridItemMenuItem{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewAnimePageButton
//
// Example:
// const downloadButton = ctx.newAnimePageButton({
// label: "Download",
// intent: "primary",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewAnimePageButton(call goja.FunctionCall) goja.Value {
// Create a new action
action := &AnimePageButton{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewAnimePageDropdownItem
//
// Example:
// const downloadButton = ctx.newAnimePageDropdownItem({
// label: "Download",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewAnimePageDropdownItem(call goja.FunctionCall) goja.Value {
// Create a new action
action := &AnimePageDropdownMenuItem{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewAnimeLibraryDropdownItem
//
// Example:
// const downloadButton = ctx.newAnimeLibraryDropdownItem({
// label: "Download",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewAnimeLibraryDropdownItem(call goja.FunctionCall) goja.Value {
// Create a new action
action := &AnimeLibraryDropdownMenuItem{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewMediaCardContextMenuItem
//
// Example:
// const downloadButton = ctx.newMediaCardContextMenuItem({
// label: "Download",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewMediaCardContextMenuItem(call goja.FunctionCall) goja.Value {
// Create a new action
action := &MediaCardContextMenuItem{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// jsNewMangaPageButton
//
// Example:
// const downloadButton = ctx.newMangaPageButton({
// label: "Download",
// onClick: "download-button-clicked",
// })
func (a *ActionManager) jsNewMangaPageButton(call goja.FunctionCall) goja.Value {
// Create a new action
action := &MangaPageButton{}
// Get the props
a.unmarshalProps(call, action)
action.ID = uuid.New().String()
// Create the object
obj := action.CreateObject(a)
return obj
}
// ///////////////////////////////////////////////////////////////////////////////////
// Shared
// ///////////////////////////////////////////////////////////////////////////////////
// bindSharedToObject binds shared methods to action objects
//
// Example:
// const downloadButton = ctx.newAnimePageButton(...)
// downloadButton.mount()
// downloadButton.unmount()
// downloadButton.setLabel("Downloading...")
func (a *ActionManager) bindSharedToObject(obj *goja.Object, action interface{}) {
var id string
var props BaseActionProps
var mapToUse interface{}
switch act := action.(type) {
case *AnimePageButton:
id = act.ID
props = act.GetProps()
mapToUse = a.animePageButtons
case *MangaPageButton:
id = act.ID
props = act.GetProps()
mapToUse = a.mangaPageButtons
case *AnimePageDropdownMenuItem:
id = act.ID
props = act.GetProps()
mapToUse = a.animePageDropdownItems
case *AnimeLibraryDropdownMenuItem:
id = act.ID
props = act.GetProps()
mapToUse = a.animeLibraryDropdownItems
case *MediaCardContextMenuItem:
id = act.ID
props = act.GetProps()
mapToUse = a.mediaCardContextMenuItems
case *EpisodeCardContextMenuItem:
id = act.ID
props = act.GetProps()
mapToUse = a.episodeCardContextMenuItems
case *EpisodeGridItemMenuItem:
id = act.ID
props = act.GetProps()
mapToUse = a.episodeGridItemMenuItems
}
_ = obj.Set("mount", func() {
switch m := mapToUse.(type) {
case *result.Map[string, *AnimePageButton]:
if btn, ok := action.(*AnimePageButton); ok {
m.Set(id, btn)
a.renderAnimePageButtons()
}
case *result.Map[string, *MangaPageButton]:
if btn, ok := action.(*MangaPageButton); ok {
m.Set(id, btn)
a.renderMangaPageButtons()
}
case *result.Map[string, *AnimePageDropdownMenuItem]:
if item, ok := action.(*AnimePageDropdownMenuItem); ok {
m.Set(id, item)
a.renderAnimePageDropdownItems()
}
case *result.Map[string, *AnimeLibraryDropdownMenuItem]:
if item, ok := action.(*AnimeLibraryDropdownMenuItem); ok {
m.Set(id, item)
a.renderAnimeLibraryDropdownItems()
}
case *result.Map[string, *MediaCardContextMenuItem]:
if item, ok := action.(*MediaCardContextMenuItem); ok {
if item.For == "" {
item.For = MediaCardContextMenuItemForBoth
}
m.Set(id, item)
a.renderMediaCardContextMenuItems()
}
case *result.Map[string, *EpisodeCardContextMenuItem]:
if item, ok := action.(*EpisodeCardContextMenuItem); ok {
m.Set(id, item)
a.renderEpisodeCardContextMenuItems()
}
case *result.Map[string, *EpisodeGridItemMenuItem]:
if item, ok := action.(*EpisodeGridItemMenuItem); ok {
m.Set(id, item)
a.renderEpisodeGridItemMenuItems()
}
}
})
_ = obj.Set("unmount", func() {
switch m := mapToUse.(type) {
case *result.Map[string, *AnimePageButton]:
m.Delete(id)
a.renderAnimePageButtons()
case *result.Map[string, *MangaPageButton]:
m.Delete(id)
a.renderMangaPageButtons()
case *result.Map[string, *AnimePageDropdownMenuItem]:
m.Delete(id)
a.renderAnimePageDropdownItems()
case *result.Map[string, *AnimeLibraryDropdownMenuItem]:
m.Delete(id)
a.renderAnimeLibraryDropdownItems()
case *result.Map[string, *MediaCardContextMenuItem]:
m.Delete(id)
a.renderMediaCardContextMenuItems()
case *result.Map[string, *EpisodeCardContextMenuItem]:
m.Delete(id)
a.renderEpisodeCardContextMenuItems()
case *result.Map[string, *EpisodeGridItemMenuItem]:
m.Delete(id)
a.renderEpisodeGridItemMenuItems()
}
})
_ = obj.Set("setLabel", func(label string) {
newProps := props
newProps.Label = label
switch act := action.(type) {
case *AnimePageButton:
act.SetProps(newProps)
case *MangaPageButton:
act.SetProps(newProps)
case *AnimePageDropdownMenuItem:
act.SetProps(newProps)
case *AnimeLibraryDropdownMenuItem:
act.SetProps(newProps)
case *MediaCardContextMenuItem:
act.SetProps(newProps)
case *EpisodeCardContextMenuItem:
act.SetProps(newProps)
case *EpisodeGridItemMenuItem:
act.SetProps(newProps)
}
})
_ = obj.Set("setStyle", func(style map[string]string) {
newProps := props
newProps.Style = style
switch act := action.(type) {
case *AnimePageButton:
act.SetProps(newProps)
a.renderAnimePageButtons()
case *MangaPageButton:
act.SetProps(newProps)
a.renderMangaPageButtons()
case *AnimePageDropdownMenuItem:
act.SetProps(newProps)
a.renderAnimePageDropdownItems()
case *AnimeLibraryDropdownMenuItem:
act.SetProps(newProps)
a.renderAnimeLibraryDropdownItems()
case *MediaCardContextMenuItem:
act.SetProps(newProps)
a.renderMediaCardContextMenuItems()
case *EpisodeCardContextMenuItem:
act.SetProps(newProps)
a.renderEpisodeCardContextMenuItems()
case *EpisodeGridItemMenuItem:
act.SetProps(newProps)
}
})
_ = obj.Set("onClick", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
a.ctx.handleTypeError("onClick requires a callback function")
}
callback, ok := goja.AssertFunction(call.Argument(0))
if !ok {
a.ctx.handleTypeError("onClick requires a callback function")
}
eventListener := a.ctx.RegisterEventListener(ClientActionClickedEvent)
eventListener.SetCallback(func(event *ClientPluginEvent) {
payload := ClientActionClickedEventPayload{}
if event.ParsePayloadAs(ClientActionClickedEvent, &payload) && payload.ActionID == id {
a.ctx.scheduler.ScheduleAsync(func() error {
_, err := callback(goja.Undefined(), a.ctx.vm.ToValue(payload.Event))
return err
})
}
})
return goja.Undefined()
})
}
/////////////////////////////////////////////////////////////////////////////////////
// Utils
/////////////////////////////////////////////////////////////////////////////////////
func (a *ActionManager) unmarshalProps(call goja.FunctionCall, ret interface{}) {
if len(call.Arguments) < 1 {
a.ctx.handleException(fmt.Errorf("expected 1 argument"))
}
props := call.Arguments[0].Export()
if props == nil {
a.ctx.handleException(fmt.Errorf("expected props object"))
}
marshaled, err := json.Marshal(props)
if err != nil {
a.ctx.handleException(err)
}
err = json.Unmarshal(marshaled, ret)
if err != nil {
a.ctx.handleException(err)
}
}