diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index e82ea93e2..ec8476342 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -394,6 +394,9 @@ input ConfigInterfaceInput { customLocales: String customLocalesEnabled: Boolean + "When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting" + disableCustomizations: Boolean + "Interface language" language: String @@ -467,6 +470,9 @@ type ConfigInterfaceResult { customLocales: String customLocalesEnabled: Boolean + "When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting" + disableCustomizations: Boolean + "Interface language" language: String diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index b39cf373a..3f10e5d3a 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -515,6 +515,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled) + r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations) + if input.DisableDropdownCreate != nil { ddc := input.DisableDropdownCreate r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 8a20fcad1..bc76212eb 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -156,6 +156,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { javascriptEnabled := config.GetJavascriptEnabled() customLocales := config.GetCustomLocales() customLocalesEnabled := config.GetCustomLocalesEnabled() + disableCustomizations := config.GetDisableCustomizations() language := config.GetLanguage() handyKey := config.GetHandyKey() scriptOffset := config.GetFunscriptOffset() @@ -183,6 +184,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { JavascriptEnabled: &javascriptEnabled, CustomLocales: &customLocales, CustomLocalesEnabled: &customLocalesEnabled, + DisableCustomizations: &disableCustomizations, Language: &language, ImageLightbox: &imageLightboxOptions, diff --git a/internal/api/server.go b/internal/api/server.go index 9290c6512..797c86ade 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -421,7 +421,7 @@ func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var paths []string - if c.GetCSSEnabled() { + if c.GetCSSEnabled() && !c.GetDisableCustomizations() { // search for custom.css in current directory, then $HOME/.stash fn := c.GetCSSPath() exists, _ := fsutil.FileExists(fn) @@ -439,7 +439,7 @@ func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Req return func(w http.ResponseWriter, r *http.Request) { var paths []string - if c.GetJavascriptEnabled() { + if c.GetJavascriptEnabled() && !c.GetDisableCustomizations() { // search for custom.js in current directory, then $HOME/.stash fn := c.GetJavascriptPath() exists, _ := fsutil.FileExists(fn) @@ -457,7 +457,7 @@ func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http. return func(w http.ResponseWriter, r *http.Request) { buffer := bytes.Buffer{} - if c.GetCustomLocalesEnabled() { + if c.GetCustomLocalesEnabled() && !c.GetDisableCustomizations() { // search for custom-locales.json in current directory, then $HOME/.stash path := c.GetCustomLocalesPath() exists, _ := fsutil.FileExists(path) diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 73b9de3ab..a0c260f23 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -194,6 +194,7 @@ const ( CSSEnabled = "cssenabled" JavascriptEnabled = "javascriptenabled" CustomLocalesEnabled = "customlocalesenabled" + DisableCustomizations = "disable_customizations" ShowScrubber = "show_scrubber" showScrubberDefault = true @@ -1457,6 +1458,13 @@ func (i *Config) GetCustomLocalesEnabled() bool { return i.getBool(CustomLocalesEnabled) } +// GetDisableCustomizations returns true if all customizations (plugins, custom CSS, +// custom JavaScript, and custom locales) should be disabled. This is useful for +// troubleshooting issues without permanently disabling individual customizations. +func (i *Config) GetDisableCustomizations() bool { + return i.getBool(DisableCustomizations) +} + func (i *Config) GetHandyKey() string { return i.getString(HandyKey) } diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index 1c3e9dc1b..05f60a6b0 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -92,6 +92,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { javascriptEnabled customLocales customLocalesEnabled + disableCustomizations language imageLightbox { slideshowDelay diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index a8b92ecc3..18ae04b8f 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -351,7 +351,12 @@ export const App: React.FC = () => { formats={intlFormats} > - + {maybeRenderReleaseNotes()} diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index bbc334a96..a9a488083 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -300,6 +300,16 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( /> + + saveInterface({ disableCustomizations: v })} + /> + + { + // Skip loading plugin JS if customizations are disabled. + // Note: We check inside useMemoOnce rather than early-returning from useLoadPlugins + // to comply with React's rules of hooks - hooks must be called unconditionally. + if (disableCustomizations) { + return [[], true]; + } return [ uniq( sortedPlugins @@ -83,9 +90,12 @@ function useLoadPlugins() { ), !!sortedPlugins && !pluginsLoading && !pluginsError, ]; - }, [sortedPlugins, pluginsLoading, pluginsError]); + }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]); const pluginCSS = useMemoOnce(() => { + if (disableCustomizations) { + return [[], true]; + } return [ uniq( sortedPlugins @@ -95,7 +105,7 @@ function useLoadPlugins() { ), !!sortedPlugins && !pluginsLoading && !pluginsError, ]; - }, [sortedPlugins, pluginsLoading, pluginsError]); + }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]); const pluginJavascriptLoaded = useScript( pluginJavascripts ?? [], @@ -109,11 +119,15 @@ function useLoadPlugins() { }; } -export const PluginsLoader: React.FC> = ({ - children, -}) => { +interface IPluginsLoaderProps { + disableCustomizations?: boolean; +} + +export const PluginsLoader: React.FC< + React.PropsWithChildren +> = ({ disableCustomizations, children }) => { const Toast = useToast(); - const { loading: loaded, error } = useLoadPlugins(); + const { loading: loaded, error } = useLoadPlugins(disableCustomizations); useEffect(() => { if (error) {