Support multiple content folders. Closes #2

This commit is contained in:
Stash Dev 2019-03-23 14:09:05 -07:00
parent d959d61e6c
commit ae9bbf237f
17 changed files with 1449 additions and 886 deletions

View file

@ -15,6 +15,10 @@ Simply run the executable (double click the exe on windows or run `./stash-osx`
*Note for Windows users:* Running the app might present a security prompt since the binary isn't signed yet. Just click more info and then the run anyway button. *Note for Windows users:* Running the app might present a security prompt since the binary isn't signed yet. Just click more info and then the run anyway button.
## Slack
I created a Slack channel to discuss the project. [Click here to join.](https://join.slack.com/stash-project/shared_invite/MTc2Nzg0NjAyNzg4LTE0OTM1ODU4MTgtNDcwODRiMGIwYQ)
#### FFMPEG #### FFMPEG
If stash is unable to find or download FFMPEG then download it yourself from the link for your platform: If stash is unable to find or download FFMPEG then download it yourself from the link for your platform:
@ -27,9 +31,7 @@ The `ffmpeg(.exe)` and `ffprobe(.exe)` files should be placed in `~/.stash` on m
# FAQ # FAQ
> Does stash support multiple folders? TODO
Not yet, but this will come in the future.
# Development # Development
@ -66,7 +68,7 @@ TODO
## Cross compiling ## Cross compiling
This project makes use of [this](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment This project uses a modification of [this](https://github.com/bep/dockerfiles/tree/master/ci-goreleaser) docker container to create an environment
where the app can be cross compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following where the app can be cross compiled. This process is kicked off by CI via the `scripts/cross-compile.sh` script. Run the following
command to open a bash shell to the container to poke around: command to open a bash shell to the container to poke around:

View file

@ -8,7 +8,7 @@ import (
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
) )
func (r *queryResolver) ConfigureGeneral(ctx context.Context, input *models.ConfigGeneralInput) (models.ConfigGeneralResult, error) { func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input *models.ConfigGeneralInput) (models.ConfigGeneralResult, error) {
if input == nil { if input == nil {
return makeConfigGeneralResult(), fmt.Errorf("nil input") return makeConfigGeneralResult(), fmt.Errorf("nil input")
} }
@ -29,9 +29,3 @@ func (r *queryResolver) ConfigureGeneral(ctx context.Context, input *models.Conf
return makeConfigGeneralResult(), nil return makeConfigGeneralResult(), nil
} }
func makeConfigGeneralResult() models.ConfigGeneralResult {
return models.ConfigGeneralResult{
Stashes: config.GetStashPaths(),
}
}

View file

@ -0,0 +1,32 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) Configuration(ctx context.Context) (models.ConfigResult, error) {
return makeConfigResult(), nil
}
func (r *queryResolver) Directories(ctx context.Context, path *string) ([]string, error) {
var dirPath = ""
if path != nil {
dirPath = *path
}
return utils.ListDir(dirPath), nil
}
func makeConfigResult() models.ConfigResult {
return models.ConfigResult{
General: makeConfigGeneralResult(),
}
}
func makeConfigGeneralResult() models.ConfigGeneralResult {
return models.ConfigGeneralResult{
Stashes: config.GetStashPaths(),
}
}

View file

@ -54,13 +54,6 @@ func initConfig() {
viper.AddConfigPath("$HOME/.stash") // Look for the config in the home directory viper.AddConfigPath("$HOME/.stash") // Look for the config in the home directory
viper.AddConfigPath(".") // Look for config in the working directory viper.AddConfigPath(".") // Look for config in the working directory
viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())
// Set generated to the metadata path for backwards compat
if !viper.IsSet(config.Generated) {
viper.SetDefault(config.Generated, viper.GetString(config.Metadata))
}
err := viper.ReadInConfig() // Find and read the config file err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file if err != nil { // Handle errors reading the config file
_ = utils.Touch(paths.GetDefaultConfigFilePath()) _ = utils.Touch(paths.GetDefaultConfigFilePath())
@ -69,6 +62,11 @@ func initConfig() {
} }
} }
viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())
// Set generated to the metadata path for backwards compat
viper.SetDefault(config.Generated, viper.GetString(config.Metadata))
// Watch for changes // Watch for changes
viper.WatchConfig() viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) { viper.OnConfigChange(func(e fsnotify.Event) {

View file

@ -1,7 +1,7 @@
package paths package paths
import ( import (
"os/user" "github.com/stashapp/stash/pkg/utils"
"path/filepath" "path/filepath"
) )
@ -25,16 +25,8 @@ func NewPaths() *Paths {
return &p return &p
} }
func GetHomeDirectory() string {
currentUser, err := user.Current()
if err != nil {
panic(err)
}
return currentUser.HomeDir
}
func GetConfigDirectory() string { func GetConfigDirectory() string {
return filepath.Join(GetHomeDirectory(), ".stash") return filepath.Join(utils.GetHomeDirectory(), ".stash")
} }
func GetDefaultDatabaseFilePath() string { func GetDefaultDatabaseFilePath() string {

View file

@ -51,6 +51,10 @@ type ComplexityRoot struct {
Stashes func(childComplexity int) int Stashes func(childComplexity int) int
} }
ConfigResult struct {
General func(childComplexity int) int
}
FindGalleriesResultType struct { FindGalleriesResultType struct {
Count func(childComplexity int) int Count func(childComplexity int) int
Galleries func(childComplexity int) int Galleries func(childComplexity int) int
@ -108,6 +112,7 @@ type ComplexityRoot struct {
TagCreate func(childComplexity int, input TagCreateInput) int TagCreate func(childComplexity int, input TagCreateInput) int
TagUpdate func(childComplexity int, input TagUpdateInput) int TagUpdate func(childComplexity int, input TagUpdateInput) int
TagDestroy func(childComplexity int, input TagDestroyInput) int TagDestroy func(childComplexity int, input TagDestroyInput) int
ConfigureGeneral func(childComplexity int, input *ConfigGeneralInput) int
} }
Performer struct { Performer struct {
@ -153,7 +158,8 @@ type ComplexityRoot struct {
SceneMarkerTags func(childComplexity int, scene_id string) int SceneMarkerTags func(childComplexity int, scene_id string) int
ScrapeFreeones func(childComplexity int, performer_name string) int ScrapeFreeones func(childComplexity int, performer_name string) int
ScrapeFreeonesPerformerList func(childComplexity int, query string) int ScrapeFreeonesPerformerList func(childComplexity int, query string) int
ConfigureGeneral func(childComplexity int, input *ConfigGeneralInput) int Configuration func(childComplexity int) int
Directories func(childComplexity int, path *string) int
MetadataImport func(childComplexity int) int MetadataImport func(childComplexity int) int
MetadataExport func(childComplexity int) int MetadataExport func(childComplexity int) int
MetadataScan func(childComplexity int) int MetadataScan func(childComplexity int) int
@ -284,6 +290,7 @@ type MutationResolver interface {
TagCreate(ctx context.Context, input TagCreateInput) (*Tag, error) TagCreate(ctx context.Context, input TagCreateInput) (*Tag, error)
TagUpdate(ctx context.Context, input TagUpdateInput) (*Tag, error) TagUpdate(ctx context.Context, input TagUpdateInput) (*Tag, error)
TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error) TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error)
ConfigureGeneral(ctx context.Context, input *ConfigGeneralInput) (ConfigGeneralResult, error)
} }
type PerformerResolver interface { type PerformerResolver interface {
ID(ctx context.Context, obj *Performer) (string, error) ID(ctx context.Context, obj *Performer) (string, error)
@ -327,7 +334,8 @@ type QueryResolver interface {
SceneMarkerTags(ctx context.Context, scene_id string) ([]SceneMarkerTag, error) SceneMarkerTags(ctx context.Context, scene_id string) ([]SceneMarkerTag, error)
ScrapeFreeones(ctx context.Context, performer_name string) (*ScrapedPerformer, error) ScrapeFreeones(ctx context.Context, performer_name string) (*ScrapedPerformer, error)
ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error) ScrapeFreeonesPerformerList(ctx context.Context, query string) ([]string, error)
ConfigureGeneral(ctx context.Context, input *ConfigGeneralInput) (ConfigGeneralResult, error) Configuration(ctx context.Context) (ConfigResult, error)
Directories(ctx context.Context, path *string) ([]string, error)
MetadataImport(ctx context.Context) (string, error) MetadataImport(ctx context.Context) (string, error)
MetadataExport(ctx context.Context) (string, error) MetadataExport(ctx context.Context) (string, error)
MetadataScan(ctx context.Context) (string, error) MetadataScan(ctx context.Context) (string, error)
@ -607,6 +615,34 @@ func (e *executableSchema) field_Mutation_tagDestroy_args(ctx context.Context, r
} }
func (e *executableSchema) field_Mutation_configureGeneral_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
args := map[string]interface{}{}
var arg0 *ConfigGeneralInput
if tmp, ok := rawArgs["input"]; ok {
var err error
var ptr1 ConfigGeneralInput
if tmp != nil {
ptr1, err = UnmarshalConfigGeneralInput(tmp)
arg0 = &ptr1
}
if err != nil {
return nil, err
}
if arg0 != nil {
var err error
arg0, err = e.ConfigGeneralInputMiddleware(ctx, arg0)
if err != nil {
return nil, err
}
}
}
args["input"] = arg0
return args, nil
}
func (e *executableSchema) field_Query_findScene_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (e *executableSchema) field_Query_findScene_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
args := map[string]interface{}{} args := map[string]interface{}{}
var arg0 *string var arg0 *string
@ -1066,30 +1102,22 @@ func (e *executableSchema) field_Query_scrapeFreeonesPerformerList_args(ctx cont
} }
func (e *executableSchema) field_Query_configureGeneral_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (e *executableSchema) field_Query_directories_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
args := map[string]interface{}{} args := map[string]interface{}{}
var arg0 *ConfigGeneralInput var arg0 *string
if tmp, ok := rawArgs["input"]; ok { if tmp, ok := rawArgs["path"]; ok {
var err error var err error
var ptr1 ConfigGeneralInput var ptr1 string
if tmp != nil { if tmp != nil {
ptr1, err = UnmarshalConfigGeneralInput(tmp) ptr1, err = graphql.UnmarshalString(tmp)
arg0 = &ptr1 arg0 = &ptr1
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if arg0 != nil {
var err error
arg0, err = e.ConfigGeneralInputMiddleware(ctx, arg0)
if err != nil {
return nil, err
} }
} args["path"] = arg0
}
args["input"] = arg0
return args, nil return args, nil
} }
@ -1159,6 +1187,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ConfigGeneralResult.Stashes(childComplexity), true return e.complexity.ConfigGeneralResult.Stashes(childComplexity), true
case "ConfigResult.general":
if e.complexity.ConfigResult.General == nil {
break
}
return e.complexity.ConfigResult.General(childComplexity), true
case "FindGalleriesResultType.count": case "FindGalleriesResultType.count":
if e.complexity.FindGalleriesResultType.Count == nil { if e.complexity.FindGalleriesResultType.Count == nil {
break break
@ -1438,6 +1473,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.TagDestroy(childComplexity, args["input"].(TagDestroyInput)), true return e.complexity.Mutation.TagDestroy(childComplexity, args["input"].(TagDestroyInput)), true
case "Mutation.configureGeneral":
if e.complexity.Mutation.ConfigureGeneral == nil {
break
}
args, err := e.field_Mutation_configureGeneral_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ConfigureGeneral(childComplexity, args["input"].(*ConfigGeneralInput)), true
case "Performer.id": case "Performer.id":
if e.complexity.Performer.Id == nil { if e.complexity.Performer.Id == nil {
break break
@ -1796,17 +1843,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.ScrapeFreeonesPerformerList(childComplexity, args["query"].(string)), true return e.complexity.Query.ScrapeFreeonesPerformerList(childComplexity, args["query"].(string)), true
case "Query.configureGeneral": case "Query.configuration":
if e.complexity.Query.ConfigureGeneral == nil { if e.complexity.Query.Configuration == nil {
break break
} }
args, err := e.field_Query_configureGeneral_args(context.TODO(), rawArgs) return e.complexity.Query.Configuration(childComplexity), true
case "Query.directories":
if e.complexity.Query.Directories == nil {
break
}
args, err := e.field_Query_directories_args(context.TODO(), rawArgs)
if err != nil { if err != nil {
return 0, false return 0, false
} }
return e.complexity.Query.ConfigureGeneral(childComplexity, args["input"].(*ConfigGeneralInput)), true return e.complexity.Query.Directories(childComplexity, args["path"].(*string)), true
case "Query.metadataImport": case "Query.metadataImport":
if e.complexity.Query.MetadataImport == nil { if e.complexity.Query.MetadataImport == nil {
@ -2494,6 +2548,62 @@ func (ec *executionContext) _ConfigGeneralResult_stashes(ctx context.Context, fi
return arr1 return arr1
} }
var configResultImplementors = []string{"ConfigResult"}
// nolint: gocyclo, errcheck, gas, goconst
func (ec *executionContext) _ConfigResult(ctx context.Context, sel ast.SelectionSet, obj *ConfigResult) graphql.Marshaler {
fields := graphql.CollectFields(ctx, sel, configResultImplementors)
out := graphql.NewFieldSet(fields)
invalid := false
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("ConfigResult")
case "general":
out.Values[i] = ec._ConfigResult_general(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalid = true
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalid {
return graphql.Null
}
return out
}
// nolint: vetshadow
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) }()
rctx := &graphql.ResolverContext{
Object: "ConfigResult",
Field: field,
Args: nil,
}
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.General, nil
})
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(ConfigGeneralResult)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec._ConfigGeneralResult(ctx, field.Selections, &res)
}
var findGalleriesResultTypeImplementors = []string{"FindGalleriesResultType"} var findGalleriesResultTypeImplementors = []string{"FindGalleriesResultType"}
// nolint: gocyclo, errcheck, gas, goconst // nolint: gocyclo, errcheck, gas, goconst
@ -3598,6 +3708,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalid = true invalid = true
} }
case "configureGeneral":
out.Values[i] = ec._Mutation_configureGeneral(ctx, field)
if out.Values[i] == graphql.Null {
invalid = true
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -4001,6 +4116,41 @@ func (ec *executionContext) _Mutation_tagDestroy(ctx context.Context, field grap
return graphql.MarshalBoolean(res) return graphql.MarshalBoolean(res)
} }
// nolint: vetshadow
func (ec *executionContext) _Mutation_configureGeneral(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,
}
ctx = graphql.WithResolverContext(ctx, rctx)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_configureGeneral_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().ConfigureGeneral(rctx, args["input"].(*ConfigGeneralInput))
})
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(ConfigGeneralResult)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec._ConfigGeneralResult(ctx, field.Selections, &res)
}
var performerImplementors = []string{"Performer"} var performerImplementors = []string{"Performer"}
// nolint: gocyclo, errcheck, gas, goconst // nolint: gocyclo, errcheck, gas, goconst
@ -4935,10 +5085,19 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
} }
return res return res
}) })
case "configureGeneral": case "configuration":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
res = ec._Query_configureGeneral(ctx, field) res = ec._Query_configuration(ctx, field)
if res == graphql.Null {
invalid = true
}
return res
})
case "directories":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
res = ec._Query_directories(ctx, field)
if res == graphql.Null { if res == graphql.Null {
invalid = true invalid = true
} }
@ -5833,7 +5992,35 @@ func (ec *executionContext) _Query_scrapeFreeonesPerformerList(ctx context.Conte
} }
// nolint: vetshadow // nolint: vetshadow
func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field graphql.CollectedField) graphql.Marshaler { func (ec *executionContext) _Query_configuration(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() { ec.Tracer.EndFieldExecution(ctx) }()
rctx := &graphql.ResolverContext{
Object: "Query",
Field: field,
Args: nil,
}
ctx = graphql.WithResolverContext(ctx, rctx)
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.Query().Configuration(rctx)
})
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(ConfigResult)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec._ConfigResult(ctx, field.Selections, &res)
}
// nolint: vetshadow
func (ec *executionContext) _Query_directories(ctx context.Context, field graphql.CollectedField) graphql.Marshaler {
ctx = ec.Tracer.StartFieldExecution(ctx, field) ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() { ec.Tracer.EndFieldExecution(ctx) }() defer func() { ec.Tracer.EndFieldExecution(ctx) }()
rctx := &graphql.ResolverContext{ rctx := &graphql.ResolverContext{
@ -5843,7 +6030,7 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
} }
ctx = graphql.WithResolverContext(ctx, rctx) ctx = graphql.WithResolverContext(ctx, rctx)
rawArgs := field.ArgumentMap(ec.Variables) rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_configureGeneral_args(ctx, rawArgs) args, err := ec.field_Query_directories_args(ctx, rawArgs)
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
return graphql.Null return graphql.Null
@ -5852,7 +6039,7 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { resTmp := ec.FieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().ConfigureGeneral(rctx, args["input"].(*ConfigGeneralInput)) return ec.resolvers.Query().Directories(rctx, args["path"].(*string))
}) })
if resTmp == nil { if resTmp == nil {
if !ec.HasError(rctx) { if !ec.HasError(rctx) {
@ -5860,11 +6047,19 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
} }
return graphql.Null return graphql.Null
} }
res := resTmp.(ConfigGeneralResult) res := resTmp.([]string)
rctx.Result = res rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx) ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec._ConfigGeneralResult(ctx, field.Selections, &res) arr1 := make(graphql.Array, len(res))
for idx1 := range res {
arr1[idx1] = func() graphql.Marshaler {
return graphql.MarshalString(res[idx1])
}()
}
return arr1
} }
// nolint: vetshadow // nolint: vetshadow
@ -11960,6 +12155,11 @@ type ConfigGeneralResult {
stashes: [String!] stashes: [String!]
} }
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}
############# #############
# Root Schema # Root Schema
############# #############
@ -12011,7 +12211,10 @@ type Query {
scrapeFreeonesPerformerList(query: String!): [String!]! scrapeFreeonesPerformerList(query: String!): [String!]!
# Config # Config
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult! """Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directories(path: String): [String!]!
# Metadata # Metadata
@ -12049,10 +12252,13 @@ type Mutation {
tagCreate(input: TagCreateInput!): Tag tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean! tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult!
} }
type Subscription { type Subscription {
"""Update from the meatadata manager""" """Update from the metadata manager"""
metadataUpdate: String! metadataUpdate: String!
} }

View file

@ -18,6 +18,11 @@ type ConfigGeneralResult struct {
Stashes []string `json:"stashes"` Stashes []string `json:"stashes"`
} }
// All configuration settings
type ConfigResult struct {
General ConfigGeneralResult `json:"general"`
}
type FindFilterType struct { type FindFilterType struct {
Q *string `json:"q"` Q *string `json:"q"`
Page *int `json:"page"` Page *int `json:"page"`

View file

@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"github.com/h2non/filetype" "github.com/h2non/filetype"
"github.com/h2non/filetype/types" "github.com/h2non/filetype/types"
"io/ioutil"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
) )
@ -84,3 +86,41 @@ func EmptyDir(path string) error {
return nil return nil
} }
func ListDir(path string) []string {
if path == "" {
path = GetHomeDirectory()
}
absolutePath, err := filepath.Abs(path)
if err == nil {
path = absolutePath
}
files, err := ioutil.ReadDir(path)
if err != nil {
path = filepath.Dir(path)
files, err = ioutil.ReadDir(path)
}
var dirPaths []string
for _, file := range files {
if !file.IsDir() {
continue
}
abs, err := filepath.Abs(path)
if err != nil {
continue
}
dirPaths = append(dirPaths, filepath.Join(abs, file.Name()))
}
return dirPaths
}
func GetHomeDirectory() string {
currentUser, err := user.Current()
if err != nil {
panic(err)
}
return currentUser.HomeDir
}

View file

@ -0,0 +1,9 @@
fragment ConfigGeneralData on ConfigGeneralResult {
stashes
}
fragment ConfigData on ConfigResult {
general {
...ConfigGeneralData
}
}

View file

@ -0,0 +1,5 @@
mutation ConfigureGeneral($input: ConfigGeneralInput!) {
configureGeneral(input: $input) {
...ConfigGeneralData
}
}

View file

@ -0,0 +1,9 @@
query Configuration {
configuration {
...ConfigData
}
}
query Directories($path: String) {
directories(path: $path)
}

View file

@ -1,3 +0,0 @@
query ConfigureGeneral($input: ConfigGeneralInput!) {
configureGeneral(input: $input)
}

View file

@ -386,6 +386,11 @@ type ConfigGeneralResult {
stashes: [String!] stashes: [String!]
} }
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}
############# #############
# Root Schema # Root Schema
############# #############
@ -437,7 +442,10 @@ type Query {
scrapeFreeonesPerformerList(query: String!): [String!]! scrapeFreeonesPerformerList(query: String!): [String!]!
# Config # Config
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult! """Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directories(path: String): [String!]!
# Metadata # Metadata
@ -475,10 +483,13 @@ type Mutation {
tagCreate(input: TagCreateInput!): Tag tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean! tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult!
} }
type Subscription { type Subscription {
"""Update from the meatadata manager""" """Update from the metadata manager"""
metadataUpdate: String! metadataUpdate: String!
} }

View file

@ -1,19 +1,73 @@
import { import {
Button,
Divider,
FormGroup,
H1, H1,
H4, H4,
H6, H6,
Spinner,
Tag, Tag,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import React, { FunctionComponent } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as GQL from "../../core/generated-graphql"; import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { TextUtils } from "../../utils/text"; import { TextUtils } from "../../utils/text";
import { ToastUtils } from "../../utils/toasts";
import { FolderSelect } from "../Shared/FolderSelect/FolderSelect";
interface IProps {} interface IProps {}
export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IProps) => { export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IProps) => {
// Editing config state
const [stashes, setStashes] = useState<string[]>([]);
// const [config, setConfig] = useState<Partial<GQL.ConfigDataFragment>>({});
const { data, error, loading } = StashService.useConfiguration();
const updateGeneralConfig = StashService.useConfigureGeneral({
stashes,
});
useEffect(() => {
if (!data || !data.configuration || !!error) { return; }
const conf = StashService.nullToUndefined(data.configuration) as GQL.ConfigDataFragment;
if (!!conf.general) {
setStashes(conf.general.stashes || []);
}
// setConfig(conf);
}, [data]);
function onStashesChanged(directories: string[]) {
setStashes(directories);
}
async function onSave() {
try {
const result = await updateGeneralConfig();
console.log(result);
ToastUtils.success("Updated config");
} catch (e) {
ErrorUtils.handle(e);
}
}
return ( return (
<> <>
Configuration {!!error ? error : undefined}
{(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
<H4>Library</H4>
<FormGroup
label="Stashes"
helperText="Directory locations to your content"
>
<FolderSelect
directories={stashes}
onDirectoriesChanged={onStashesChanged}
/>
</FormGroup>
<Divider />
<Button intent="primary" onClick={() => onSave()}>Save</Button>
</> </>
); );
}; };

View file

@ -0,0 +1,85 @@
import {
Button,
Classes,
Dialog,
InputGroup,
Spinner,
} from "@blueprintjs/core";
import _ from "lodash";
import React, { FunctionComponent, useEffect, useState } from "react";
import { StashService } from "../../../core/StashService";
interface IProps {
directories: string[];
onDirectoriesChanged: (directories: string[]) => void;
}
export const FolderSelect: FunctionComponent<IProps> = (props: IProps) => {
const [currentDirectory, setCurrentDirectory] = useState<string>("");
const [isDisplayingDialog, setIsDisplayingDialog] = useState<boolean>(false);
const [selectableDirectories, setSelectableDirectories] = useState<string[]>([]);
const [selectedDirectories, setSelectedDirectories] = useState<string[]>([]);
const { data, error, loading } = StashService.useDirectories(currentDirectory);
useEffect(() => {
setSelectedDirectories(props.directories);
}, [props.directories]);
useEffect(() => {
if (!data || !data.directories || !!error) { return; }
setSelectableDirectories(StashService.nullToUndefined(data.directories));
}, [data]);
function onSelectDirectory() {
selectedDirectories.push(currentDirectory);
setSelectedDirectories(selectedDirectories);
setCurrentDirectory("");
setIsDisplayingDialog(false);
props.onDirectoriesChanged(selectedDirectories);
}
function onRemoveDirectory(directory: string) {
const newSelectedDirectories = selectedDirectories.filter((dir) => dir !== directory);
setSelectedDirectories(newSelectedDirectories);
props.onDirectoriesChanged(newSelectedDirectories);
}
function renderDialog() {
return (
<Dialog
isOpen={isDisplayingDialog}
onClose={() => setIsDisplayingDialog(false)}
title="Select Directory"
>
<div className="dialog-content">
<InputGroup
large={true}
placeholder="File path"
onChange={(e: any) => setCurrentDirectory(e.target.value)}
value={currentDirectory}
rightElement={(!data || !data.directories || loading) ? <Spinner size={Spinner.SIZE_SMALL} /> : undefined}
/>
{selectableDirectories.map((path) => {
return <div key={path} onClick={() => setCurrentDirectory(path)}>{path}</div>;
})}
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={() => onSelectDirectory()}>Add</Button>
</div>
</div>
</Dialog>
);
}
return (
<>
{!!error ? error : undefined}
{renderDialog()}
{selectedDirectories.map((path) => {
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
})}
<Button small={true} onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>
</>
);
};

View file

@ -118,6 +118,9 @@ export class StashService {
} }
public static useStats() { return GQL.useStats(); } public static useStats() { return GQL.useStats(); }
public static useConfiguration() { return GQL.useConfiguration(); }
public static useDirectories(path?: string) { return GQL.useDirectories({ variables: { path }}); }
public static usePerformerCreate(input: GQL.PerformerCreateInput) { public static usePerformerCreate(input: GQL.PerformerCreateInput) {
return GQL.usePerformerCreate({ variables: input }); return GQL.usePerformerCreate({ variables: input });
} }
@ -143,6 +146,10 @@ export class StashService {
return GQL.useTagUpdate({ variables: input, refetchQueries: ["AllTags"] }); return GQL.useTagUpdate({ variables: input, refetchQueries: ["AllTags"] });
} }
public static useConfigureGeneral(input: GQL.ConfigGeneralInput) {
return GQL.useConfigureGeneral({ variables: { input }, refetchQueries: ["Configuration"] });
}
public static queryScrapeFreeones(performerName: string) { public static queryScrapeFreeones(performerName: string) {
return StashService.client.query<GQL.ScrapeFreeonesQuery>({ return StashService.client.query<GQL.ScrapeFreeonesQuery>({
query: GQL.ScrapeFreeonesDocument, query: GQL.ScrapeFreeonesDocument,

File diff suppressed because it is too large Load diff