diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index d2c6c20b1..87d12fcaf 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -4,8 +4,16 @@ fragment ConfigGeneralData on ConfigGeneralResult { generatedPath } +fragment ConfigInterfaceData on ConfigInterfaceResult { + css + cssEnabled +} + fragment ConfigData on ConfigResult { general { ...ConfigGeneralData } + interface { + ...ConfigInterfaceData + } } \ No newline at end of file diff --git a/graphql/documents/mutations/config.graphql b/graphql/documents/mutations/config.graphql index 9081cb8a2..4e273f418 100644 --- a/graphql/documents/mutations/config.graphql +++ b/graphql/documents/mutations/config.graphql @@ -2,4 +2,10 @@ mutation ConfigureGeneral($input: ConfigGeneralInput!) { configureGeneral(input: $input) { ...ConfigGeneralData } +} + +mutation ConfigureInterface($input: ConfigInterfaceInput!) { + configureInterface(input: $input) { + ...ConfigInterfaceData + } } \ No newline at end of file diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index e756cd285..1bac03071 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -91,6 +91,7 @@ type Mutation { """Change general configuration options""" configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult! + configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult! } type Subscription { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index a7390c5e7..351fc844e 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -16,7 +16,20 @@ type ConfigGeneralResult { generatedPath: String! } +input ConfigInterfaceInput { + """Custom CSS""" + css: String + cssEnabled: Boolean +} + +type ConfigInterfaceResult { + """Custom CSS""" + css: String + cssEnabled: Boolean +} + """All configuration settings""" type ConfigResult { general: ConfigGeneralResult! + interface: ConfigInterfaceResult! } \ No newline at end of file diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index b7db89f05..7dff1b713 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -3,10 +3,11 @@ package api import ( "context" "fmt" + "path/filepath" + "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" - "path/filepath" ) func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) { @@ -41,3 +42,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co return makeConfigGeneralResult(), nil } + +func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) { + css := "" + + if input.CSS != nil { + css = *input.CSS + } + + config.SetCSS(css) + + if input.CSSEnabled != nil { + config.Set(config.CSSEnabled, *input.CSSEnabled) + } + + if err := config.Write(); err != nil { + return makeConfigInterfaceResult(), err + } + + return makeConfigInterfaceResult(), nil +} diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index e2ae353a0..7b96cd5f4 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -2,6 +2,7 @@ package api import ( "context" + "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" @@ -21,7 +22,8 @@ func (r *queryResolver) Directories(ctx context.Context, path *string) ([]string func makeConfigResult() *models.ConfigResult { return &models.ConfigResult{ - General: makeConfigGeneralResult(), + General: makeConfigGeneralResult(), + Interface: makeConfigInterfaceResult(), } } @@ -32,3 +34,12 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult { GeneratedPath: config.GetGeneratedPath(), } } + +func makeConfigInterfaceResult() *models.ConfigInterfaceResult { + css := config.GetCSS() + cssEnabled := config.GetCSSEnabled() + return &models.ConfigInterfaceResult{ + CSS: &css, + CSSEnabled: &cssEnabled, + } +} diff --git a/pkg/api/server.go b/pkg/api/server.go index e75298540..5f662c783 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -5,6 +5,15 @@ import ( "crypto/tls" "errors" "fmt" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "runtime/debug" + "strconv" + "strings" + "github.com/99designs/gqlgen/handler" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -16,14 +25,6 @@ import ( "github.com/stashapp/stash/pkg/manager/paths" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" - "io/ioutil" - "net/http" - "os" - "path" - "path/filepath" - "runtime/debug" - "strconv" - "strings" ) var uiBox *packr.Box @@ -74,6 +75,21 @@ func Start() { r.Mount("/scene", sceneRoutes{}.Routes()) r.Mount("/studio", studioRoutes{}.Routes()) + r.HandleFunc("/css", func(w http.ResponseWriter, r *http.Request) { + if !config.GetCSSEnabled() { + return + } + + // search for custom.css in current directory, then $HOME/.stash + fn := config.GetCSSPath() + exists, _ := utils.FileExists(fn) + if !exists { + return + } + + http.ServeFile(w, r, fn) + }) + // Serve the setup UI r.HandleFunc("/setup*", func(w http.ResponseWriter, r *http.Request) { ext := path.Ext(r.URL.Path) diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index f54caa6a9..fa3d3ffce 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -1,7 +1,10 @@ package config import ( + "io/ioutil" "github.com/spf13/viper" + + "github.com/stashapp/stash/pkg/utils" ) const Stash = "stash" @@ -15,6 +18,8 @@ const Database = "database" const Host = "host" const Port = "port" +const CSSEnabled = "cssEnabled" + func Set(key string, value interface{}) { viper.Set(key, value) } @@ -51,6 +56,46 @@ func GetPort() int { return viper.GetInt(Port) } +func GetCSSPath() string { + // search for custom.css in current directory, then $HOME/.stash + fn := "custom.css" + exists, _ := utils.FileExists(fn) + if !exists { + fn = "$HOME/.stash/" + fn + } + + return fn +} + +func GetCSS() string { + fn := GetCSSPath() + + exists, _ := utils.FileExists(fn) + if !exists { + return "" + } + + buf, err := ioutil.ReadFile(fn) + + if err != nil { + return "" + } + + return string(buf) +} + +func SetCSS(css string) { + fn := GetCSSPath() + + buf := []byte(css) + + ioutil.WriteFile(fn, buf, 0777) +} + +func GetCSSEnabled() bool { + return viper.GetBool(CSSEnabled) +} + func IsValid() bool { setPaths := viper.IsSet(Stash) && viper.IsSet(Cache) && viper.IsSet(Generated) && viper.IsSet(Metadata) // TODO: check valid paths diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index 5e9fb218e..c2b7a316d 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -1,8 +1,9 @@ package manager import ( - "fmt" - "github.com/fsnotify/fsnotify" + "net" + "sync" + "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stashapp/stash/pkg/ffmpeg" @@ -10,8 +11,6 @@ import ( "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/manager/paths" "github.com/stashapp/stash/pkg/utils" - "net" - "sync" ) type singleton struct { @@ -71,12 +70,15 @@ func initConfig() { // Set generated to the metadata path for backwards compat viper.SetDefault(config.Generated, viper.GetString(config.Metadata)) + // Disabling config watching due to race condition issue + // See: https://github.com/spf13/viper/issues/174 + // Changes to the config outside the system will require a restart // Watch for changes - viper.WatchConfig() - viper.OnConfigChange(func(e fsnotify.Event) { - fmt.Println("Config file changed:", e.Name) - instance.refreshConfig() - }) + // viper.WatchConfig() + // viper.OnConfigChange(func(e fsnotify.Event) { + // fmt.Println("Config file changed:", e.Name) + // instance.refreshConfig() + // }) //viper.Set("stash", []string{"/", "/stuff"}) //viper.WriteConfig() @@ -92,15 +94,15 @@ func initFlags() { } } -func initEnvs() { +func initEnvs() { viper.SetEnvPrefix("stash") // will be uppercased automatically - viper.BindEnv("host") // STASH_HOST - viper.BindEnv("port") // STASH_PORT - viper.BindEnv("stash") // STASH_STASH - viper.BindEnv("generated") // STASH_GENERATED - viper.BindEnv("metadata") // STASH_METADATA - viper.BindEnv("cache") // STASH_CACHE -} + viper.BindEnv("host") // STASH_HOST + viper.BindEnv("port") // STASH_PORT + viper.BindEnv("stash") // STASH_STASH + viper.BindEnv("generated") // STASH_GENERATED + viper.BindEnv("metadata") // STASH_METADATA + viper.BindEnv("cache") // STASH_CACHE +} func initFFMPEG() { configDirectory := paths.GetConfigDirectory() diff --git a/pkg/models/generated_exec.go b/pkg/models/generated_exec.go index 56a4abecc..ed9050e4a 100644 --- a/pkg/models/generated_exec.go +++ b/pkg/models/generated_exec.go @@ -56,8 +56,14 @@ type ComplexityRoot struct { Stashes func(childComplexity int) int } + ConfigInterfaceResult struct { + CSS func(childComplexity int) int + CSSEnabled func(childComplexity int) int + } + ConfigResult struct { - General func(childComplexity int) int + General func(childComplexity int) int + Interface func(childComplexity int) int } FindGalleriesResultType struct { @@ -107,6 +113,7 @@ type ComplexityRoot struct { Mutation struct { ConfigureGeneral func(childComplexity int, input ConfigGeneralInput) int + ConfigureInterface func(childComplexity int, input ConfigInterfaceInput) int PerformerCreate func(childComplexity int, input PerformerCreateInput) int PerformerDestroy func(childComplexity int, input PerformerDestroyInput) int PerformerUpdate func(childComplexity int, input PerformerUpdateInput) int @@ -298,6 +305,7 @@ type MutationResolver interface { TagUpdate(ctx context.Context, input TagUpdateInput) (*Tag, error) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) ConfigureGeneral(ctx context.Context, input ConfigGeneralInput) (*ConfigGeneralResult, error) + ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) } type PerformerResolver interface { Name(ctx context.Context, obj *Performer) (*string, error) @@ -424,6 +432,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ConfigGeneralResult.Stashes(childComplexity), true + case "ConfigInterfaceResult.css": + if e.complexity.ConfigInterfaceResult.CSS == nil { + break + } + + return e.complexity.ConfigInterfaceResult.CSS(childComplexity), true + + case "ConfigInterfaceResult.cssEnabled": + if e.complexity.ConfigInterfaceResult.CSSEnabled == nil { + break + } + + return e.complexity.ConfigInterfaceResult.CSSEnabled(childComplexity), true + case "ConfigResult.general": if e.complexity.ConfigResult.General == nil { break @@ -431,6 +453,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ConfigResult.General(childComplexity), true + case "ConfigResult.interface": + if e.complexity.ConfigResult.Interface == nil { + break + } + + return e.complexity.ConfigResult.Interface(childComplexity), true + case "FindGalleriesResultType.count": if e.complexity.FindGalleriesResultType.Count == nil { break @@ -590,6 +619,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ConfigureGeneral(childComplexity, args["input"].(ConfigGeneralInput)), true + case "Mutation.configureInterface": + if e.complexity.Mutation.ConfigureInterface == nil { + break + } + + args, err := ec.field_Mutation_configureInterface_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ConfigureInterface(childComplexity, args["input"].(ConfigInterfaceInput)), true + case "Mutation.performerCreate": if e.complexity.Mutation.PerformerCreate == nil { break @@ -1880,6 +1921,7 @@ type Mutation { """Change general configuration options""" configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult! + configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult! } type Subscription { @@ -1910,9 +1952,22 @@ type ConfigGeneralResult { generatedPath: String! } +input ConfigInterfaceInput { + """Custom CSS""" + css: String + cssEnabled: Boolean +} + +type ConfigInterfaceResult { + """Custom CSS""" + css: String + cssEnabled: Boolean +} + """All configuration settings""" type ConfigResult { general: ConfigGeneralResult! + interface: ConfigInterfaceResult! }`}, &ast.Source{Name: "graphql/schema/types/filters.graphql", Input: `enum SortDirectionEnum { ASC @@ -2294,6 +2349,20 @@ func (ec *executionContext) field_Mutation_configureGeneral_args(ctx context.Con return args, nil } +func (ec *executionContext) field_Mutation_configureInterface_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 ConfigInterfaceInput + if tmp, ok := rawArgs["input"]; ok { + arg0, err = ec.unmarshalNConfigInterfaceInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_performerCreate_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2917,6 +2986,54 @@ func (ec *executionContext) _ConfigGeneralResult_generatedPath(ctx context.Conte return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _ConfigInterfaceResult_css(ctx context.Context, field graphql.CollectedField, obj *ConfigInterfaceResult) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "ConfigInterfaceResult", + Field: field, + Args: nil, + IsMethod: false, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CSS, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _ConfigInterfaceResult_cssEnabled(ctx context.Context, field graphql.CollectedField, obj *ConfigInterfaceResult) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "ConfigInterfaceResult", + Field: field, + Args: nil, + IsMethod: false, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CSSEnabled, nil + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*bool) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res) +} + func (ec *executionContext) _ConfigResult_general(ctx context.Context, field graphql.CollectedField, obj *ConfigResult) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -2944,6 +3061,33 @@ func (ec *executionContext) _ConfigResult_general(ctx context.Context, field gra return ec.marshalNConfigGeneralResult2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigGeneralResult(ctx, field.Selections, res) } +func (ec *executionContext) _ConfigResult_interface(ctx context.Context, field graphql.CollectedField, obj *ConfigResult) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "ConfigResult", + Field: field, + Args: nil, + IsMethod: false, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Interface, nil + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ConfigInterfaceResult) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNConfigInterfaceResult2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceResult(ctx, field.Selections, res) +} + func (ec *executionContext) _FindGalleriesResultType_count(ctx context.Context, field graphql.CollectedField, obj *FindGalleriesResultType) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -3951,6 +4095,40 @@ func (ec *executionContext) _Mutation_configureGeneral(ctx context.Context, fiel return ec.marshalNConfigGeneralResult2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigGeneralResult(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_configureInterface(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { ec.Tracer.EndFieldExecution(ctx) }() + rctx := &graphql.ResolverContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_configureInterface_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + rctx.Args = args + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().ConfigureInterface(rctx, args["input"].(ConfigInterfaceInput)) + }) + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ConfigInterfaceResult) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNConfigInterfaceResult2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceResult(ctx, field.Selections, res) +} + func (ec *executionContext) _Performer_id(ctx context.Context, field graphql.CollectedField, obj *Performer) graphql.Marshaler { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { ec.Tracer.EndFieldExecution(ctx) }() @@ -8049,6 +8227,30 @@ func (ec *executionContext) unmarshalInputConfigGeneralInput(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputConfigInterfaceInput(ctx context.Context, v interface{}) (ConfigInterfaceInput, error) { + var it ConfigInterfaceInput + var asMap = v.(map[string]interface{}) + + for k, v := range asMap { + switch k { + case "css": + var err error + it.CSS, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "cssEnabled": + var err error + it.CSSEnabled, err = ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputFindFilterType(ctx context.Context, v interface{}) (FindFilterType, error) { var it FindFilterType var asMap = v.(map[string]interface{}) @@ -8868,6 +9070,32 @@ func (ec *executionContext) _ConfigGeneralResult(ctx context.Context, sel ast.Se return out } +var configInterfaceResultImplementors = []string{"ConfigInterfaceResult"} + +func (ec *executionContext) _ConfigInterfaceResult(ctx context.Context, sel ast.SelectionSet, obj *ConfigInterfaceResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.RequestContext, sel, configInterfaceResultImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ConfigInterfaceResult") + case "css": + out.Values[i] = ec._ConfigInterfaceResult_css(ctx, field, obj) + case "cssEnabled": + out.Values[i] = ec._ConfigInterfaceResult_cssEnabled(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var configResultImplementors = []string{"ConfigResult"} func (ec *executionContext) _ConfigResult(ctx context.Context, sel ast.SelectionSet, obj *ConfigResult) graphql.Marshaler { @@ -8884,6 +9112,11 @@ func (ec *executionContext) _ConfigResult(ctx context.Context, sel ast.Selection if out.Values[i] == graphql.Null { invalids++ } + case "interface": + out.Values[i] = ec._ConfigResult_interface(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -9243,6 +9476,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "configureInterface": + out.Values[i] = ec._Mutation_configureInterface(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10843,6 +11081,24 @@ func (ec *executionContext) marshalNConfigGeneralResult2ᚖgithubᚗcomᚋstasha return ec._ConfigGeneralResult(ctx, sel, v) } +func (ec *executionContext) unmarshalNConfigInterfaceInput2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceInput(ctx context.Context, v interface{}) (ConfigInterfaceInput, error) { + return ec.unmarshalInputConfigInterfaceInput(ctx, v) +} + +func (ec *executionContext) marshalNConfigInterfaceResult2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceResult(ctx context.Context, sel ast.SelectionSet, v ConfigInterfaceResult) graphql.Marshaler { + return ec._ConfigInterfaceResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNConfigInterfaceResult2ᚖgithubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigInterfaceResult(ctx context.Context, sel ast.SelectionSet, v *ConfigInterfaceResult) graphql.Marshaler { + if v == nil { + if !ec.HasError(graphql.GetResolverContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._ConfigInterfaceResult(ctx, sel, v) +} + func (ec *executionContext) marshalNConfigResult2githubᚗcomᚋstashappᚋstashᚋpkgᚋmodelsᚐConfigResult(ctx context.Context, sel ast.SelectionSet, v ConfigResult) graphql.Marshaler { return ec._ConfigResult(ctx, sel, &v) } diff --git a/pkg/models/generated_models.go b/pkg/models/generated_models.go index 49b7b5d75..4d37c3b05 100644 --- a/pkg/models/generated_models.go +++ b/pkg/models/generated_models.go @@ -26,9 +26,22 @@ type ConfigGeneralResult struct { GeneratedPath string `json:"generatedPath"` } +type ConfigInterfaceInput struct { + // Custom CSS + CSS *string `json:"css"` + CSSEnabled *bool `json:"cssEnabled"` +} + +type ConfigInterfaceResult struct { + // Custom CSS + CSS *string `json:"css"` + CSSEnabled *bool `json:"cssEnabled"` +} + // All configuration settings type ConfigResult struct { - General *ConfigGeneralResult `json:"general"` + General *ConfigGeneralResult `json:"general"` + Interface *ConfigInterfaceResult `json:"interface"` } type FindFilterType struct { diff --git a/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx b/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx index 4ba7cf954..39982ccf2 100644 --- a/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx +++ b/ui/v2/src/components/Settings/SettingsInterfacePanel.tsx @@ -1,19 +1,54 @@ import { + Button, Checkbox, + Divider, FormGroup, H4, + Spinner, + TextArea } from "@blueprintjs/core"; import _ from "lodash"; -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, useEffect, useState } from "react"; import { useInterfaceLocalForage } from "../../hooks/LocalForage"; +import { StashService } from "../../core/StashService"; +import { ErrorUtils } from "../../utils/errors"; +import { ToastUtils } from "../../utils/toasts"; interface IProps {} export const SettingsInterfacePanel: FunctionComponent = () => { const {data, setData} = useInterfaceLocalForage(); + const config = StashService.useConfiguration(); + const [css, setCSS] = useState(); + const [cssEnabled, setCSSEnabled] = useState(); + + const updateInterfaceConfig = StashService.useConfigureInterface({ + css, + cssEnabled + }); + + useEffect(() => { + if (!config.data || !config.data.configuration || !!config.error) { return; } + if (!!config.data.configuration.interface) { + setCSS(config.data.configuration.interface.css || ""); + setCSSEnabled(config.data.configuration.interface.cssEnabled || false); + } + }, [config.data]); + + async function onSave() { + try { + const result = await updateInterfaceConfig(); + console.log(result); + ToastUtils.success("Updated config"); + } catch (e) { + ErrorUtils.handle(e); + } + } return ( <> + {!!config.error ?

{config.error.message}

: undefined} + {(!config.data || !config.data.configuration || config.loading) ? : undefined}

User Interface

= () => { }} /> + + + { + setCSSEnabled(!cssEnabled) + }} + /> + + + + + + ); }; diff --git a/ui/v2/src/components/Wall/WallItem.tsx b/ui/v2/src/components/Wall/WallItem.tsx index cc1a7fc5b..a4d800d73 100644 --- a/ui/v2/src/components/Wall/WallItem.tsx +++ b/ui/v2/src/components/Wall/WallItem.tsx @@ -1,5 +1,5 @@ import _ from "lodash"; -import React, { FunctionComponent, useRef, useState } from "react"; +import React, { FunctionComponent, useRef, useState, useEffect } from "react"; import { Link } from "react-router-dom"; import * as GQL from "../../core/generated-graphql"; import { useInterfaceLocalForage } from "../../hooks/LocalForage"; @@ -17,6 +17,10 @@ interface IWallItemProps { export const WallItem: FunctionComponent = (props: IWallItemProps) => { const [videoPath, setVideoPath] = useState(undefined); + const [previewPath, setPreviewPath] = useState(""); + const [screenshotPath, setScreenshotPath] = useState(""); + const [title, setTitle] = useState(""); + const [tags, setTags] = useState([]); const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true}); const interfaceSettings = useInterfaceLocalForage(); const showTextContainer = !!interfaceSettings.data ? interfaceSettings.data.wall.textContainerEnabled : true; @@ -68,18 +72,25 @@ export const WallItem: FunctionComponent = (props: IWallItemProp } } - let previewSrc: string = ""; - let title: string = ""; - let tags: JSX.Element[] = []; - if (!!props.sceneMarker) { - previewSrc = props.sceneMarker.preview; - title = `${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`; - tags = props.sceneMarker.tags.map((tag) => ({tag.name})); - tags.unshift({props.sceneMarker.primary_tag.name}); - } else if (!!props.scene) { - previewSrc = props.scene.paths.webp || ""; - title = props.scene.title || ""; - // tags = props.scene.tags.map((tag) => ({tag.name})); + useEffect(() => { + if (!!props.sceneMarker) { + setPreviewPath(props.sceneMarker.preview); + setTitle(`${props.sceneMarker!.title} - ${TextUtils.secondsToTimestamp(props.sceneMarker.seconds)}`); + const thisTags = props.sceneMarker.tags.map((tag) => ({tag.name})); + thisTags.unshift({props.sceneMarker.primary_tag.name}); + setTags(thisTags); + } else if (!!props.scene) { + setPreviewPath(props.scene.paths.webp || ""); + setScreenshotPath(props.scene.paths.screenshot || ""); + setTitle(props.scene.title || ""); + // tags = props.scene.tags.map((tag) => ({tag.name})); + } + }, [props.sceneMarker, props.scene]); + + function previewNotFound() { + if (previewPath !== screenshotPath) { + setPreviewPath(screenshotPath); + } } const className = ["scene-wall-item-container"]; @@ -99,12 +110,13 @@ export const WallItem: FunctionComponent = (props: IWallItemProp onClick()} to={linkSrc}>