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 <>>;
+ }}
+ />
+