diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 6241a283c..57f08eb41 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -63,6 +63,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { showStudioAsText css cssEnabled + javascript + javascriptEnabled customLocales customLocalesEnabled language diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 48f2de3dc..7cd1fea5f 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -264,6 +264,10 @@ input ConfigInterfaceInput { css: String cssEnabled: Boolean + """Custom Javascript""" + javascript: String + javascriptEnabled: Boolean + """Custom Locales""" customLocales: String customLocalesEnabled: Boolean @@ -330,6 +334,10 @@ type ConfigInterfaceResult { css: String cssEnabled: Boolean + """Custom Javascript""" + javascript: String + javascriptEnabled: Boolean + """Custom Locales""" customLocales: String customLocalesEnabled: Boolean diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 0252192e7..0cbc0209b 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -365,6 +365,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI setBool(config.CSSEnabled, input.CSSEnabled) + if input.Javascript != nil { + c.SetJavascript(*input.Javascript) + } + + setBool(config.JavascriptEnabled, input.JavascriptEnabled) + if input.CustomLocales != nil { c.SetCustomLocales(*input.CustomLocales) } diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 64a6f0364..941fb9a49 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -142,6 +142,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { showStudioAsText := config.GetShowStudioAsText() css := config.GetCSS() cssEnabled := config.GetCSSEnabled() + javascript := config.GetJavascript() + javascriptEnabled := config.GetJavascriptEnabled() customLocales := config.GetCustomLocales() customLocalesEnabled := config.GetCustomLocalesEnabled() language := config.GetLanguage() @@ -166,6 +168,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { ContinuePlaylistDefault: &continuePlaylistDefault, CSS: &css, CSSEnabled: &cssEnabled, + Javascript: &javascript, + JavascriptEnabled: &javascriptEnabled, CustomLocales: &customLocales, CustomLocalesEnabled: &customLocalesEnabled, Language: &language, diff --git a/internal/api/server.go b/internal/api/server.go index 5f7c96c85..62d50bb94 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -181,6 +181,21 @@ func Start() error { http.ServeFile(w, r, fn) }) + r.HandleFunc("/javascript", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/javascript") + if !c.GetJavascriptEnabled() { + return + } + + // search for custom.js in current directory, then $HOME/.stash + fn := c.GetJavascriptPath() + exists, _ := fsutil.FileExists(fn) + if !exists { + return + } + + http.ServeFile(w, r, fn) + }) r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if c.GetCustomLocalesEnabled() { diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 6e453bed5..73c116bc8 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -139,6 +139,7 @@ const ( ContinuePlaylistDefault = "continue_playlist_default" ShowStudioAsText = "show_studio_as_text" CSSEnabled = "cssEnabled" + JavascriptEnabled = "javascriptEnabled" CustomLocalesEnabled = "customLocalesEnabled" ShowScrubber = "show_scrubber" @@ -1077,6 +1078,49 @@ func (i *Instance) GetCSSEnabled() bool { return i.getBool(CSSEnabled) } +func (i *Instance) GetJavascriptPath() string { + // use custom.js in the same directory as the config file + configFileUsed := i.GetConfigFile() + configDir := filepath.Dir(configFileUsed) + + fn := filepath.Join(configDir, "custom.js") + + return fn +} + +func (i *Instance) GetJavascript() string { + fn := i.GetJavascriptPath() + + exists, _ := fsutil.FileExists(fn) + if !exists { + return "" + } + + buf, err := os.ReadFile(fn) + + if err != nil { + return "" + } + + return string(buf) +} + +func (i *Instance) SetJavascript(javascript string) { + fn := i.GetJavascriptPath() + i.Lock() + defer i.Unlock() + + buf := []byte(javascript) + + if err := os.WriteFile(fn, buf, 0777); err != nil { + logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err) + } +} + +func (i *Instance) GetJavascriptEnabled() bool { + return i.getBool(JavascriptEnabled) +} + func (i *Instance) GetCustomLocalesPath() string { // use custom-locales.json in the same directory as the config file configFileUsed := i.GetConfigFile() diff --git a/internal/manager/config/config_concurrency_test.go b/internal/manager/config/config_concurrency_test.go index 7b60bfb4c..81bb7e816 100644 --- a/internal/manager/config/config_concurrency_test.go +++ b/internal/manager/config/config_concurrency_test.go @@ -86,6 +86,8 @@ func TestConcurrentConfigAccess(t *testing.T) { i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay) i.GetCSSPath() i.GetCSS() + i.GetJavascriptPath() + i.GetJavascript() i.GetCustomLocalesPath() i.GetCustomLocales() i.Set(CSSEnabled, i.GetCSSEnabled()) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 74f510ff1..4a52d6a2b 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -510,6 +510,36 @@ export const SettingsInterfacePanel: React.FC = () => { }} /> + + saveInterface({ javascriptEnabled: v })} + /> + + + id="custom-javascript" + headingID="config.ui.custom_javascript.heading" + subHeadingID="config.ui.custom_javascript.description" + value={iface.javascript ?? undefined} + onChange={(v) => saveInterface({ javascript: v })} + renderField={(value, setValue) => ( + ) => + setValue(e.currentTarget.value) + } + rows={16} + className="text-input code" + /> + )} + renderValue={() => { + return <>; + }} + /> +