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.
## Slack
I created a Slack channel to discuss the project. [Click here to join.](https://join.slack.com/stash-project/shared_invite/MTc2Nzg0NjAyNzg4LTE0OTM1ODU4MTgtNDcwODRiMGIwYQ)
#### FFMPEG
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
> Does stash support multiple folders?
Not yet, but this will come in the future.
TODO
# Development
@ -66,7 +68,7 @@ TODO
## 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
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"
)
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 {
return makeConfigGeneralResult(), fmt.Errorf("nil input")
}
@ -29,9 +29,3 @@ func (r *queryResolver) ConfigureGeneral(ctx context.Context, input *models.Conf
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(".") // 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
if err != nil { // Handle errors reading the config file
_ = 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
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {

View file

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

View file

@ -51,6 +51,10 @@ type ComplexityRoot struct {
Stashes func(childComplexity int) int
}
ConfigResult struct {
General func(childComplexity int) int
}
FindGalleriesResultType struct {
Count func(childComplexity int) int
Galleries func(childComplexity int) int
@ -108,6 +112,7 @@ type ComplexityRoot struct {
TagCreate func(childComplexity int, input TagCreateInput) int
TagUpdate func(childComplexity int, input TagUpdateInput) int
TagDestroy func(childComplexity int, input TagDestroyInput) int
ConfigureGeneral func(childComplexity int, input *ConfigGeneralInput) int
}
Performer struct {
@ -153,7 +158,8 @@ type ComplexityRoot struct {
SceneMarkerTags func(childComplexity int, scene_id string) int
ScrapeFreeones func(childComplexity int, performer_name 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
MetadataExport func(childComplexity int) int
MetadataScan func(childComplexity int) int
@ -284,6 +290,7 @@ type MutationResolver interface {
TagCreate(ctx context.Context, input TagCreateInput) (*Tag, error)
TagUpdate(ctx context.Context, input TagUpdateInput) (*Tag, error)
TagDestroy(ctx context.Context, input TagDestroyInput) (bool, error)
ConfigureGeneral(ctx context.Context, input *ConfigGeneralInput) (ConfigGeneralResult, error)
}
type PerformerResolver interface {
ID(ctx context.Context, obj *Performer) (string, error)
@ -327,7 +334,8 @@ type QueryResolver interface {
SceneMarkerTags(ctx context.Context, scene_id string) ([]SceneMarkerTag, error)
ScrapeFreeones(ctx context.Context, performer_name string) (*ScrapedPerformer, 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)
MetadataExport(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) {
args := map[string]interface{}{}
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{}{}
var arg0 *ConfigGeneralInput
if tmp, ok := rawArgs["input"]; ok {
var arg0 *string
if tmp, ok := rawArgs["path"]; ok {
var err error
var ptr1 ConfigGeneralInput
var ptr1 string
if tmp != nil {
ptr1, err = UnmarshalConfigGeneralInput(tmp)
ptr1, err = graphql.UnmarshalString(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
args["path"] = arg0
return args, nil
}
@ -1159,6 +1187,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.FindGalleriesResultType.Count == nil {
break
@ -1438,6 +1473,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.Performer.Id == nil {
break
@ -1796,17 +1843,24 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.ScrapeFreeonesPerformerList(childComplexity, args["query"].(string)), true
case "Query.configureGeneral":
if e.complexity.Query.ConfigureGeneral == nil {
case "Query.configuration":
if e.complexity.Query.Configuration == nil {
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 {
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":
if e.complexity.Query.MetadataImport == nil {
@ -2494,6 +2548,62 @@ func (ec *executionContext) _ConfigGeneralResult_stashes(ctx context.Context, fi
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"}
// 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 {
invalid = true
}
case "configureGeneral":
out.Values[i] = ec._Mutation_configureGeneral(ctx, field)
if out.Values[i] == graphql.Null {
invalid = true
}
default:
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)
}
// 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"}
// nolint: gocyclo, errcheck, gas, goconst
@ -4935,10 +5085,19 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
}
return res
})
case "configureGeneral":
case "configuration":
field := field
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 {
invalid = true
}
@ -5833,7 +5992,35 @@ func (ec *executionContext) _Query_scrapeFreeonesPerformerList(ctx context.Conte
}
// 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)
defer func() { ec.Tracer.EndFieldExecution(ctx) }()
rctx := &graphql.ResolverContext{
@ -5843,7 +6030,7 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
}
ctx = graphql.WithResolverContext(ctx, rctx)
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 {
ec.Error(ctx, err)
return graphql.Null
@ -5852,7 +6039,7 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
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().ConfigureGeneral(rctx, args["input"].(*ConfigGeneralInput))
return ec.resolvers.Query().Directories(rctx, args["path"].(*string))
})
if resTmp == nil {
if !ec.HasError(rctx) {
@ -5860,11 +6047,19 @@ func (ec *executionContext) _Query_configureGeneral(ctx context.Context, field g
}
return graphql.Null
}
res := resTmp.(ConfigGeneralResult)
res := resTmp.([]string)
rctx.Result = res
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
@ -11960,6 +12155,11 @@ type ConfigGeneralResult {
stashes: [String!]
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}
#############
# Root Schema
#############
@ -12011,7 +12211,10 @@ type Query {
scrapeFreeonesPerformerList(query: String!): [String!]!
# 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
@ -12049,10 +12252,13 @@ type Mutation {
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult!
}
type Subscription {
"""Update from the meatadata manager"""
"""Update from the metadata manager"""
metadataUpdate: String!
}

View file

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

View file

@ -4,7 +4,9 @@ import (
"fmt"
"github.com/h2non/filetype"
"github.com/h2non/filetype/types"
"io/ioutil"
"os"
"os/user"
"path/filepath"
)
@ -84,3 +86,41 @@ func EmptyDir(path string) error {
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!]
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}
#############
# Root Schema
#############
@ -437,7 +442,10 @@ type Query {
scrapeFreeonesPerformerList(query: String!): [String!]!
# 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
@ -475,10 +483,13 @@ type Mutation {
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput): ConfigGeneralResult!
}
type Subscription {
"""Update from the meatadata manager"""
"""Update from the metadata manager"""
metadataUpdate: String!
}

View file

@ -1,19 +1,73 @@
import {
Button,
Divider,
FormGroup,
H1,
H4,
H6,
Spinner,
Tag,
} from "@blueprintjs/core";
import React, { FunctionComponent } from "react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as GQL from "../../core/generated-graphql";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { TextUtils } from "../../utils/text";
import { ToastUtils } from "../../utils/toasts";
import { FolderSelect } from "../Shared/FolderSelect/FolderSelect";
interface 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 (
<>
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 useConfiguration() { return GQL.useConfiguration(); }
public static useDirectories(path?: string) { return GQL.useDirectories({ variables: { path }}); }
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
return GQL.usePerformerCreate({ variables: input });
}
@ -143,6 +146,10 @@ export class StashService {
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) {
return StashService.client.query<GQL.ScrapeFreeonesQuery>({
query: GQL.ScrapeFreeonesDocument,

File diff suppressed because it is too large Load diff