This commit is contained in:
Gykes 2025-12-04 03:04:27 +02:00 committed by GitHub
commit c602de5b78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 66 additions and 11 deletions

View file

@ -394,6 +394,9 @@ input ConfigInterfaceInput {
customLocales: String customLocales: String
customLocalesEnabled: Boolean customLocalesEnabled: Boolean
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
disableCustomizations: Boolean
"Interface language" "Interface language"
language: String language: String
@ -467,6 +470,9 @@ type ConfigInterfaceResult {
customLocales: String customLocales: String
customLocalesEnabled: Boolean customLocalesEnabled: Boolean
"When true, disables all customizations (plugins, CSS, JavaScript, locales) for troubleshooting"
disableCustomizations: Boolean
"Interface language" "Interface language"
language: String language: String

View file

@ -515,6 +515,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled) r.setConfigBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
r.setConfigBool(config.DisableCustomizations, input.DisableCustomizations)
if input.DisableDropdownCreate != nil { if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate ddc := input.DisableDropdownCreate
r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer) r.setConfigBool(config.DisableDropdownCreatePerformer, ddc.Performer)

View file

@ -156,6 +156,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
javascriptEnabled := config.GetJavascriptEnabled() javascriptEnabled := config.GetJavascriptEnabled()
customLocales := config.GetCustomLocales() customLocales := config.GetCustomLocales()
customLocalesEnabled := config.GetCustomLocalesEnabled() customLocalesEnabled := config.GetCustomLocalesEnabled()
disableCustomizations := config.GetDisableCustomizations()
language := config.GetLanguage() language := config.GetLanguage()
handyKey := config.GetHandyKey() handyKey := config.GetHandyKey()
scriptOffset := config.GetFunscriptOffset() scriptOffset := config.GetFunscriptOffset()
@ -183,6 +184,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
JavascriptEnabled: &javascriptEnabled, JavascriptEnabled: &javascriptEnabled,
CustomLocales: &customLocales, CustomLocales: &customLocales,
CustomLocalesEnabled: &customLocalesEnabled, CustomLocalesEnabled: &customLocalesEnabled,
DisableCustomizations: &disableCustomizations,
Language: &language, Language: &language,
ImageLightbox: &imageLightboxOptions, ImageLightbox: &imageLightboxOptions,

View file

@ -421,7 +421,7 @@ func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
var paths []string var paths []string
if c.GetCSSEnabled() { if c.GetCSSEnabled() && !c.GetDisableCustomizations() {
// search for custom.css in current directory, then $HOME/.stash // search for custom.css in current directory, then $HOME/.stash
fn := c.GetCSSPath() fn := c.GetCSSPath()
exists, _ := fsutil.FileExists(fn) 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) { return func(w http.ResponseWriter, r *http.Request) {
var paths []string var paths []string
if c.GetJavascriptEnabled() { if c.GetJavascriptEnabled() && !c.GetDisableCustomizations() {
// search for custom.js in current directory, then $HOME/.stash // search for custom.js in current directory, then $HOME/.stash
fn := c.GetJavascriptPath() fn := c.GetJavascriptPath()
exists, _ := fsutil.FileExists(fn) 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) { return func(w http.ResponseWriter, r *http.Request) {
buffer := bytes.Buffer{} buffer := bytes.Buffer{}
if c.GetCustomLocalesEnabled() { if c.GetCustomLocalesEnabled() && !c.GetDisableCustomizations() {
// search for custom-locales.json in current directory, then $HOME/.stash // search for custom-locales.json in current directory, then $HOME/.stash
path := c.GetCustomLocalesPath() path := c.GetCustomLocalesPath()
exists, _ := fsutil.FileExists(path) exists, _ := fsutil.FileExists(path)

View file

@ -194,6 +194,7 @@ const (
CSSEnabled = "cssenabled" CSSEnabled = "cssenabled"
JavascriptEnabled = "javascriptenabled" JavascriptEnabled = "javascriptenabled"
CustomLocalesEnabled = "customlocalesenabled" CustomLocalesEnabled = "customlocalesenabled"
DisableCustomizations = "disable_customizations"
ShowScrubber = "show_scrubber" ShowScrubber = "show_scrubber"
showScrubberDefault = true showScrubberDefault = true
@ -1457,6 +1458,13 @@ func (i *Config) GetCustomLocalesEnabled() bool {
return i.getBool(CustomLocalesEnabled) 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 { func (i *Config) GetHandyKey() string {
return i.getString(HandyKey) return i.getString(HandyKey)
} }

View file

@ -92,6 +92,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
javascriptEnabled javascriptEnabled
customLocales customLocales
customLocalesEnabled customLocalesEnabled
disableCustomizations
language language
imageLightbox { imageLightbox {
slideshowDelay slideshowDelay

View file

@ -351,7 +351,12 @@ export const App: React.FC = () => {
formats={intlFormats} formats={intlFormats}
> >
<ToastProvider> <ToastProvider>
<PluginsLoader> <PluginsLoader
disableCustomizations={
config.data?.configuration?.interface?.disableCustomizations ??
false
}
>
<AppContainer> <AppContainer>
<ConfigurationProvider configuration={config.data!.configuration}> <ConfigurationProvider configuration={config.data!.configuration}>
{maybeRenderReleaseNotes()} {maybeRenderReleaseNotes()}

View file

@ -300,6 +300,16 @@ export const SettingsInterfacePanel: React.FC = PatchComponent(
/> />
</SettingSection> </SettingSection>
<SettingSection headingID="config.ui.troubleshooting.heading">
<BooleanSetting
id="disable-customizations"
headingID="config.ui.troubleshooting.disable_customizations.heading"
subHeadingID="config.ui.troubleshooting.disable_customizations.description"
checked={iface.disableCustomizations ?? undefined}
onChange={(v) => saveInterface({ disableCustomizations: v })}
/>
</SettingSection>
<SettingSection headingID="config.ui.scene_wall.heading"> <SettingSection headingID="config.ui.scene_wall.heading">
<BooleanSetting <BooleanSetting
id="wall-show-title" id="wall-show-title"

View file

@ -611,6 +611,13 @@
"heading": "Custom CSS", "heading": "Custom CSS",
"option_label": "Custom CSS enabled" "option_label": "Custom CSS enabled"
}, },
"troubleshooting": {
"heading": "Troubleshooting",
"disable_customizations": {
"heading": "Disable customizations",
"description": "Temporarily disables all plugins, custom CSS, custom JavaScript, and custom locales. Use this to troubleshoot issues without permanently disabling individual customizations. A page reload is required for changes to take effect."
}
},
"custom_javascript": { "custom_javascript": {
"description": "Page must be reloaded for changes to take effect. There is no guarantee of compatibility between custom Javascript and future releases of Stash.", "description": "Page must be reloaded for changes to take effect. There is no guarantee of compatibility between custom Javascript and future releases of Stash.",
"heading": "Custom Javascript", "heading": "Custom Javascript",

View file

@ -59,7 +59,8 @@ function sortPlugins(plugins: PluginList) {
// load all plugins and their dependencies // load all plugins and their dependencies
// returns true when all plugins are loaded, regardess of success or failure // returns true when all plugins are loaded, regardess of success or failure
function useLoadPlugins() { // if disableCustomizations is true, skip loading plugins entirely
function useLoadPlugins(disableCustomizations?: boolean) {
const { const {
data: plugins, data: plugins,
loading: pluginsLoading, loading: pluginsLoading,
@ -74,6 +75,12 @@ function useLoadPlugins() {
}, [plugins?.plugins, pluginsLoading, pluginsError]); }, [plugins?.plugins, pluginsLoading, pluginsError]);
const pluginJavascripts = useMemoOnce(() => { const pluginJavascripts = useMemoOnce(() => {
// 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 [ return [
uniq( uniq(
sortedPlugins sortedPlugins
@ -83,9 +90,12 @@ function useLoadPlugins() {
), ),
!!sortedPlugins && !pluginsLoading && !pluginsError, !!sortedPlugins && !pluginsLoading && !pluginsError,
]; ];
}, [sortedPlugins, pluginsLoading, pluginsError]); }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]);
const pluginCSS = useMemoOnce(() => { const pluginCSS = useMemoOnce(() => {
if (disableCustomizations) {
return [[], true];
}
return [ return [
uniq( uniq(
sortedPlugins sortedPlugins
@ -95,7 +105,7 @@ function useLoadPlugins() {
), ),
!!sortedPlugins && !pluginsLoading && !pluginsError, !!sortedPlugins && !pluginsLoading && !pluginsError,
]; ];
}, [sortedPlugins, pluginsLoading, pluginsError]); }, [sortedPlugins, pluginsLoading, pluginsError, disableCustomizations]);
const pluginJavascriptLoaded = useScript( const pluginJavascriptLoaded = useScript(
pluginJavascripts ?? [], pluginJavascripts ?? [],
@ -109,11 +119,15 @@ function useLoadPlugins() {
}; };
} }
export const PluginsLoader: React.FC<React.PropsWithChildren<{}>> = ({ interface IPluginsLoaderProps {
children, disableCustomizations?: boolean;
}) => { }
export const PluginsLoader: React.FC<
React.PropsWithChildren<IPluginsLoaderProps>
> = ({ disableCustomizations, children }) => {
const Toast = useToast(); const Toast = useToast();
const { loading: loaded, error } = useLoadPlugins(); const { loading: loaded, error } = useLoadPlugins(disableCustomizations);
useEffect(() => { useEffect(() => {
if (error) { if (error) {