diff --git a/.gitignore b/.gitignore index d3a1c21f0..7b392ac70 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ # GraphQL generated output internal/api/generated_*.go -ui/v2.5/src/core/generated-*.tsx #### # Jetbrains diff --git a/README.md b/README.md index 87889fe38..5f2c0fdcd 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,11 @@ https://stashapp.cc [![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml) [![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/stash 'DockerHub') +[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp) [![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash) [![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest) +[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty) ### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.** ![demo image](docs/readme_assets/demo_image.png) diff --git a/go.mod b/go.mod index facca58ce..8155adaeb 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/vearutop/statigz v1.1.6 github.com/vektah/dataloaden v0.3.0 github.com/vektah/gqlparser/v2 v2.4.1 + github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e gopkg.in/guregu/null.v4 v4.0.0 ) diff --git a/go.sum b/go.sum index def03da0a..25007a2b1 100644 --- a/go.sum +++ b/go.sum @@ -760,6 +760,8 @@ github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rty github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks= github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw= +github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= diff --git a/internal/api/resolver_query_find_gallery.go b/internal/api/resolver_query_find_gallery.go index 6474cc03e..db1fcafaf 100644 --- a/internal/api/resolver_query_find_gallery.go +++ b/internal/api/resolver_query_find_gallery.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Gallery.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_find_image.go b/internal/api/resolver_query_find_image.go index 6d33e8820..e979f3f11 100644 --- a/internal/api/resolver_query_find_image.go +++ b/internal/api/resolver_query_find_image.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/99designs/gqlgen/graphql" @@ -23,7 +25,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str } image, err = qb.Find(ctx, idInt) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } } else if checksum != nil { diff --git a/internal/api/resolver_query_find_movie.go b/internal/api/resolver_query_find_movie.go index a7e72dbdc..a728089cc 100644 --- a/internal/api/resolver_query_find_movie.go +++ b/internal/api/resolver_query_find_movie.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Movie.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } @@ -34,7 +36,6 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi Count: total, Movies: movies, } - return nil }); err != nil { return nil, err diff --git a/internal/api/resolver_query_find_performer.go b/internal/api/resolver_query_find_performer.go index 437ac8fcf..b94d67e94 100644 --- a/internal/api/resolver_query_find_performer.go +++ b/internal/api/resolver_query_find_performer.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Performer.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index 4f196fd65..6098decea 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } return ret, err @@ -40,7 +42,7 @@ func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.Filte if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.SavedFilter.FindDefault(ctx, mode) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } return ret, err diff --git a/internal/api/resolver_query_find_scene.go b/internal/api/resolver_query_find_scene.go index db72232d3..1eaa2dc03 100644 --- a/internal/api/resolver_query_find_scene.go +++ b/internal/api/resolver_query_find_scene.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/99designs/gqlgen/graphql" @@ -21,7 +23,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str return err } scene, err = qb.Find(ctx, idInt) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } } else if checksum != nil { diff --git a/internal/api/resolver_query_find_studio.go b/internal/api/resolver_query_find_studio.go index 51cac6208..3f4260bce 100644 --- a/internal/api/resolver_query_find_studio.go +++ b/internal/api/resolver_query_find_studio.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -17,7 +19,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models. var err error ret, err = r.repository.Studio.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/resolver_query_find_tag.go b/internal/api/resolver_query_find_tag.go index fd4b04ad2..9ea16525a 100644 --- a/internal/api/resolver_query_find_tag.go +++ b/internal/api/resolver_query_find_tag.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "errors" "strconv" "github.com/stashapp/stash/pkg/models" @@ -16,7 +18,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag if err := r.withReadTxn(ctx, func(ctx context.Context) error { ret, err = r.repository.Tag.Find(ctx, idInt) return err - }); err != nil { + }); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, err } diff --git a/internal/api/server.go b/internal/api/server.go index 22bb14ad0..0f6e26f52 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -339,6 +339,9 @@ func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []str } } + // Always revalidate with server + w.Header().Set("Cache-Control", "no-cache") + bufferReader := bytes.NewReader(buffer.Bytes()) http.ServeContent(w, r, name, latestModTime, bufferReader) } diff --git a/internal/manager/task_generate_preview.go b/internal/manager/task_generate_preview.go index fe5f0884d..c81909417 100644 --- a/internal/manager/task_generate_preview.go +++ b/internal/manager/task_generate_preview.go @@ -44,7 +44,7 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) { return } - if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration); err != nil { + if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration, videoFile.FrameRate); err != nil { logger.Errorf("error generating preview: %v", err) logErrorOutput(err) return @@ -59,12 +59,18 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) { } } -func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64) error { +func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64, videoFrameRate float64) error { videoFilename := t.Scene.Path + useVsync2 := false - if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false); err != nil { + if videoFrameRate <= 0.01 { + logger.Errorf("[generator] Video framerate very low/high (%f) most likely vfr so using -vsync 2", videoFrameRate) + useVsync2 = true + } + + if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false, useVsync2); err != nil { logger.Warnf("[generator] failed generating scene preview, trying fallback") - if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true); err != nil { + if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true, useVsync2); err != nil { return err } } diff --git a/pkg/file/scan.go b/pkg/file/scan.go index 31cd50af6..18c1955d2 100644 --- a/pkg/file/scan.go +++ b/pkg/file/scan.go @@ -14,6 +14,7 @@ import ( "github.com/remeh/sizedwaitgroup" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/txn" + "github.com/stashapp/stash/pkg/utils" ) const ( @@ -574,7 +575,7 @@ func (s *scanJob) handleFile(ctx context.Context, f scanFile) error { // scan zip files with a different context that is not cancellable // cancelling while scanning zip file contents results in the scan // contents being partially completed - zipCtx := context.Background() + zipCtx := utils.ValueOnlyContext{Context: ctx} if err := s.scanZipFile(zipCtx, f); err != nil { logger.Errorf("Error scanning zip file %q: %v", f.Path, err) diff --git a/pkg/file/zip.go b/pkg/file/zip.go index 1e61d340e..aee8dfdef 100644 --- a/pkg/file/zip.go +++ b/pkg/file/zip.go @@ -2,11 +2,18 @@ package file import ( "archive/zip" + "bytes" "errors" "fmt" "io" "io/fs" "path/filepath" + + "github.com/stashapp/stash/pkg/logger" + "github.com/xWTF/chardet" + + "golang.org/x/net/html/charset" + "golang.org/x/text/transform" ) var ( @@ -40,6 +47,42 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) { return nil, err } + // Concat all Name and Comment for better detection result + var buffer bytes.Buffer + for _, f := range zipReader.File { + buffer.WriteString(f.Name) + buffer.WriteString(f.Comment) + } + buffer.WriteString(zipReader.Comment) + + // Detect encoding + d, err := chardet.NewTextDetector().DetectBest(buffer.Bytes()) + if err != nil { + reader.Close() + return nil, fmt.Errorf("unable to detect decoding: %w", err) + } + + // If the charset is not UTF8, decode'em + if d.Charset != "UTF-8" { + logger.Debugf("Detected non-utf8 zip charset %s (%s): %s", d.Charset, d.Language, path) + + e, _ := charset.Lookup(d.Charset) + if e == nil { + reader.Close() + return nil, fmt.Errorf("failed to lookup charset %s, language %s", d.Charset, d.Language) + } + + decoder := e.NewDecoder() + for _, f := range zipReader.File { + f.Name, _, err = transform.String(decoder, f.Name) + if err != nil { + reader.Close() + return nil, fmt.Errorf("failed to decode %v: %w", []byte(f.Name), err) + } + // Comments are not decoded cuz stash doesn't use that + } + } + return &ZipFS{ Reader: zipReader, zipFileCloser: reader, diff --git a/pkg/job/context.go b/pkg/job/context.go deleted file mode 100644 index e53625e23..000000000 --- a/pkg/job/context.go +++ /dev/null @@ -1,22 +0,0 @@ -package job - -import ( - "context" - "time" -) - -type valueOnlyContext struct { - context.Context -} - -func (valueOnlyContext) Deadline() (deadline time.Time, ok bool) { - return -} - -func (valueOnlyContext) Done() <-chan struct{} { - return nil -} - -func (valueOnlyContext) Err() error { - return nil -} diff --git a/pkg/job/manager.go b/pkg/job/manager.go index ce5fd4f9d..4ad9b1880 100644 --- a/pkg/job/manager.go +++ b/pkg/job/manager.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/utils" ) const maxGraveyardSize = 10 @@ -178,7 +179,7 @@ func (m *Manager) dispatch(ctx context.Context, j *Job) (done chan struct{}) { j.StartTime = &t j.Status = StatusRunning - ctx, cancelFunc := context.WithCancel(valueOnlyContext{ctx}) + ctx, cancelFunc := context.WithCancel(utils.ValueOnlyContext{Context: ctx}) j.cancelFunc = cancelFunc done = make(chan struct{}) diff --git a/pkg/scene/generate/preview.go b/pkg/scene/generate/preview.go index fc82ebdc5..ceefd617c 100644 --- a/pkg/scene/generate/preview.go +++ b/pkg/scene/generate/preview.go @@ -68,7 +68,7 @@ func (g PreviewOptions) getStepSizeAndOffset(videoDuration float64) (stepSize fl return } -func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool) error { +func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool, useVsync2 bool) error { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() @@ -81,7 +81,7 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration logger.Infof("[generator] generating video preview for %s", input) - if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback)); err != nil { + if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback, useVsync2)); err != nil { return err } @@ -90,10 +90,10 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration return nil } -func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn { +func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn { // #2496 - generate a single preview video for videos shorter than segments * segment duration if videoDuration < options.SegmentDuration*float64(options.Segments) { - return g.previewVideoSingle(input, videoDuration, options, fallback) + return g.previewVideoSingle(input, videoDuration, options, fallback, useVsync2) } return func(lockCtx *fsutil.LockContext, tmpFn string) error { @@ -131,7 +131,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr Preset: options.Preset, } - if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback); err != nil { + if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2); err != nil { return err } } @@ -150,7 +150,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr } } -func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn { +func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn { return func(lockCtx *fsutil.LockContext, tmpFn string) error { chunkOptions := previewChunkOptions{ StartTime: 0, @@ -160,7 +160,7 @@ func (g *Generator) previewVideoSingle(input string, videoDuration float64, opti Preset: options.Preset, } - return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback) + return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2) } } @@ -172,7 +172,7 @@ type previewChunkOptions struct { Preset string } -func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool) error { +func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool, useVsync2 bool) error { var videoFilter ffmpeg.VideoFilter videoFilter = videoFilter.ScaleWidth(scenePreviewWidth) @@ -189,6 +189,10 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt "-strict", "-2", ) + if useVsync2 { + videoArgs = append(videoArgs, "-vsync", "2") + } + trimOptions := transcoder.TranscodeOptions{ OutputPath: options.OutputPath, StartTime: options.StartTime, diff --git a/pkg/scraper/cookies.go b/pkg/scraper/cookies.go index 72855441f..b5822b62c 100644 --- a/pkg/scraper/cookies.go +++ b/pkg/scraper/cookies.go @@ -69,7 +69,7 @@ var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123 func randomSequence(n int) string { b := make([]rune, n) - rand.Seed(time.Now().UnixNano()) + rand := rand.New(rand.NewSource(time.Now().UnixNano())) for i := range b { b[i] = characters[rand.Intn(len(characters))] } diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index a1f756766..69b200614 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -707,6 +707,13 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images) } + if ss.URL == nil && len(s.Urls) > 0 { + // The scene in Stash-box may not have a Studio URL but it does have another URL. + // For example it has a www.manyvids.com URL, which is auto set as type ManyVids. + // This should be re-visited once Stashapp can support more than one URL. + ss.URL = &s.Urls[0].URL + } + if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error { pqb := c.repository.Performer tqb := c.repository.Tag diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index e6aef4404..e7657677d 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "path/filepath" + "sort" "strconv" "strings" "time" @@ -1706,5 +1707,26 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int) ([][]*mo } } + sortByPath(duplicates) + return duplicates, nil } + +func sortByPath(scenes [][]*models.Scene) { + lessFunc := func(i int, j int) bool { + firstPathI := getFirstPath(scenes[i]) + firstPathJ := getFirstPath(scenes[j]) + return firstPathI < firstPathJ + } + sort.SliceStable(scenes, lessFunc) +} + +func getFirstPath(scenes []*models.Scene) string { + var firstPath string + for i, scene := range scenes { + if i == 0 || scene.Path < firstPath { + firstPath = scene.Path + } + } + return firstPath +} diff --git a/pkg/utils/context.go b/pkg/utils/context.go new file mode 100644 index 000000000..2a3862b5d --- /dev/null +++ b/pkg/utils/context.go @@ -0,0 +1,22 @@ +package utils + +import ( + "context" + "time" +) + +type ValueOnlyContext struct { + context.Context +} + +func (ValueOnlyContext) Deadline() (deadline time.Time, ok bool) { + return +} + +func (ValueOnlyContext) Done() <-chan struct{} { + return nil +} + +func (ValueOnlyContext) Err() error { + return nil +} diff --git a/ui/v2.5/.env b/ui/v2.5/.env index 66df2e28e..6e49b46e4 100644 --- a/ui/v2.5/.env +++ b/ui/v2.5/.env @@ -1,3 +1,2 @@ BROWSER=none -PORT=3000 ESLINT_NO_DEV_ERRORS=true diff --git a/ui/v2.5/.eslintrc.json b/ui/v2.5/.eslintrc.json index ca3e8c0ce..4438860ea 100644 --- a/ui/v2.5/.eslintrc.json +++ b/ui/v2.5/.eslintrc.json @@ -9,61 +9,69 @@ "parserOptions": { "project": "./tsconfig.json" }, - "plugins": [ - "@typescript-eslint", - "jsx-a11y" - ], + "plugins": ["@typescript-eslint", "jsx-a11y"], "extends": [ - "airbnb-typescript", - "airbnb/hooks", - "plugin:react/recommended", - "plugin:import/recommended", - "prettier", - "prettier/prettier" + "airbnb-typescript", + "plugin:import/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "airbnb/hooks", + "prettier" ], "settings": { "react": { "version": "detect" } }, + "ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"], "rules": { - "@typescript-eslint/no-explicit-any": 2, - "@typescript-eslint/naming-convention": [ - "error", - { + "@typescript-eslint/no-explicit-any": 2, + "@typescript-eslint/naming-convention": [ + "error", + { "selector": "interface", "format": ["PascalCase"], "custom": { "regex": "^I[A-Z]", "match": true - } } - ], - "lines-between-class-members": "off", - "@typescript-eslint/lines-between-class-members": "off", - "import/extensions": [ - "error", - "ignorePackages", - { - "js": "never", - "jsx": "never", - "ts": "never", - "tsx": "never" - } - ], - "import/named": "off", - "import/namespace": "off", - "import/no-unresolved": "off", - "react/display-name": "off", - "react/prop-types": "off", - "react/style-prop-object": ["error", { + } + ], + "lines-between-class-members": "off", + "@typescript-eslint/lines-between-class-members": "off", + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never" + } + ], + "import/named": "off", + "import/namespace": "off", + "import/no-unresolved": "off", + "react/display-name": "off", + "react/prop-types": "off", + "react/style-prop-object": [ + "error", + { "allow": ["FormattedNumber"] - }], - "spaced-comment": ["error", "always", { - "markers": ["/"] - }], - "prefer-destructuring": ["error", {"object": true, "array": false}], - "@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }], - "no-nested-ternary": "off" + } + ], + "spaced-comment": [ + "error", + "always", + { + "markers": ["/"] + } + ], + "prefer-destructuring": ["error", { "object": true, "array": false }], + "@typescript-eslint/no-use-before-define": [ + "error", + { "functions": false, "classes": true } + ], + "no-nested-ternary": "off" } } diff --git a/ui/v2.5/.gitignore b/ui/v2.5/.gitignore index d7b8f1bd7..baf52f432 100755 --- a/ui/v2.5/.gitignore +++ b/ui/v2.5/.gitignore @@ -1,4 +1,5 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# generated +src/core/generated-*.tsx # dependencies /node_modules @@ -12,6 +13,7 @@ /build # misc +.gitignore .DS_Store .env.local .env.development.local @@ -23,3 +25,4 @@ yarn-debug.log* yarn-error.log* .eslintcache +.stylelintcache diff --git a/ui/v2.5/.prettierignore b/ui/v2.5/.prettierignore new file mode 100644 index 000000000..aeb40cd1c --- /dev/null +++ b/ui/v2.5/.prettierignore @@ -0,0 +1,18 @@ +*.md + +# dependencies +/node_modules +/.pnp +.pnp.js + +# locales +src/locales/**/*.json + +# testing +/coverage + +# production +/build + +# generated +src/core/generated-graphql.tsx \ No newline at end of file diff --git a/ui/v2.5/.stylelintrc b/ui/v2.5/.stylelintrc index 1357ed90e..de2f58dac 100644 --- a/ui/v2.5/.stylelintrc +++ b/ui/v2.5/.stylelintrc @@ -1,91 +1,55 @@ { - "plugins": [ - "stylelint-order" - ], - "extends": "stylelint-config-prettier", + "plugins": ["stylelint-order"], + "customSyntax": "postcss-scss", "rules": { - "indentation": null, - "at-rule-empty-line-before": [ "always", { - except: ["after-same-name", "first-nested" ], - ignore: ["after-comment"], - } ], + "at-rule-empty-line-before": [ + "always", + { + "except": ["after-same-name", "first-nested"], + "ignore": ["after-comment"] + } + ], "at-rule-no-vendor-prefix": true, "selector-no-vendor-prefix": true, - "block-closing-brace-newline-after": "always", - "block-closing-brace-newline-before": "always-multi-line", - "block-closing-brace-space-before": "always-single-line", "block-no-empty": true, - "block-opening-brace-newline-after": "always-multi-line", - "block-opening-brace-space-after": "always-single-line", - "block-opening-brace-space-before": "always", - "color-hex-case": "lower", "color-hex-length": "short", "color-no-invalid-hex": true, - "comment-empty-line-before": [ "always", { - except: ["first-nested"], - ignore: ["stylelint-commands"], - } ], + "comment-empty-line-before": [ + "always", + { + "except": ["first-nested"], + "ignore": ["stylelint-commands"] + } + ], "comment-whitespace-inside": "always", - "declaration-bang-space-after": "never", - "declaration-bang-space-before": "always", "declaration-block-no-shorthand-property-overrides": true, - "declaration-block-semicolon-newline-after": "always-multi-line", - "declaration-block-semicolon-space-after": "always-single-line", - "declaration-block-semicolon-space-before": "never", "declaration-block-single-line-max-declarations": 1, - "declaration-block-trailing-semicolon": "always", - "declaration-colon-space-after": "always-single-line", - "declaration-colon-space-before": "never", "declaration-no-important": true, "font-family-name-quotes": "always-where-recommended", "function-calc-no-unspaced-operator": true, - "function-comma-newline-after": "always-multi-line", - "function-comma-space-after": "always-single-line", - "function-comma-space-before": "never", "function-linear-gradient-no-nonstandard-direction": true, - "function-parentheses-newline-inside": "always-multi-line", - "function-parentheses-space-inside": "never-single-line", "function-url-quotes": "always", - "function-whitespace-after": "always", "length-zero-no-unit": true, - "max-empty-lines": 1, "max-nesting-depth": 4, - "media-feature-colon-space-after": "always", - "media-feature-colon-space-before": "never", - "media-feature-range-operator-space-after": "always", - "media-feature-range-operator-space-before": "always", - "media-query-list-comma-newline-after": "always-multi-line", - "media-query-list-comma-space-after": "always-single-line", - "media-query-list-comma-space-before": "never", - "media-feature-parentheses-space-inside": "never", "no-descending-specificity": null, "no-invalid-double-slash-comments": true, - "no-missing-end-of-source-newline": true, "number-max-precision": 3, - "number-no-trailing-zeros": true, - "order/order": [ - "custom-properties", - "declarations" - ], + "order/order": ["custom-properties", "declarations"], "order/properties-alphabetical-order": true, - "rule-empty-line-before": ["always-multi-line", { - except: ["after-single-line-comment", "first-nested" ], - ignore: ["after-comment"], - }], + "rule-empty-line-before": [ + "always-multi-line", + { + "except": ["after-single-line-comment", "first-nested"], + "ignore": ["after-comment"] + } + ], "selector-max-id": 1, "selector-max-type": 2, "selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$", - "selector-combinator-space-after": "always", - "selector-combinator-space-before": "always", - "selector-list-comma-newline-after": "always", - "selector-list-comma-space-before": "never", "selector-max-universal": 0, "selector-type-case": "lower", "selector-pseudo-element-colon-notation": "double", "string-no-newline": true, - "string-quotes": "double", - "time-min-milliseconds": 100, - "value-list-comma-space-after": "always-single-line", - "value-list-comma-space-before": "never" - }, + "time-min-milliseconds": 100 + } } diff --git a/ui/v2.5/.vscode/launch.json b/ui/v2.5/.vscode/launch.json deleted file mode 100644 index a0b21cbef..000000000 --- a/ui/v2.5/.vscode/launch.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Chrome", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/src", - "sourceMapPathOverrides": { - "webpack:///src/*": "${webRoot}/*" - } - } - ] -} \ No newline at end of file diff --git a/ui/v2.5/.vscode/settings.json b/ui/v2.5/.vscode/settings.json deleted file mode 100644 index ba5be62ee..000000000 --- a/ui/v2.5/.vscode/settings.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "editor.tabSize": 2, - "editor.renderWhitespace": "boundary", - "editor.wordWrap": "bounded", - "javascript.preferences.importModuleSpecifier": "relative", - "typescript.preferences.importModuleSpecifier": "relative", - "editor.wordWrapColumn": 120, - "editor.rulers": [ - 120 - ], - "i18n-ally.localesPaths": [ - "src/locales" - ], - "i18n-ally.keystyle": "nested", - "i18n-ally.sourceLanguage": "en-GB", - "spellright.language": [ - "en" - ], - "spellright.documentTypes": [ - "markdown", - "latex", - "plaintext", - "typescriptreact" - ] -} \ No newline at end of file diff --git a/ui/v2.5/codegen.yml b/ui/v2.5/codegen.yml index d648406a9..08b8c54fd 100644 --- a/ui/v2.5/codegen.yml +++ b/ui/v2.5/codegen.yml @@ -4,11 +4,9 @@ documents: "../../graphql/documents/**/*.graphql" generates: src/core/generated-graphql.tsx: plugins: - - add: - content: "/* eslint-disable */" - time - typescript - typescript-operations - typescript-react-apollo config: - withRefetchFn: true + withRefetchFn: true diff --git a/ui/v2.5/index.html b/ui/v2.5/index.html index b25ebc3d0..11bbb270d 100755 --- a/ui/v2.5/index.html +++ b/ui/v2.5/index.html @@ -1,7 +1,7 @@ - + @@ -10,27 +10,15 @@ content="width=device-width, initial-scale=1, maximum-scale=1" /> - Stash - +
- diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 21c25ab58..07fe4457a 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -3,125 +3,116 @@ "version": "0.1.0", "private": true, "homepage": "./", - "sideEffects": false, "scripts": { "start": "vite", "build": "vite build", - "build-ci": "yarn validate && yarn build", - "validate": "yarn lint && tsc --noEmit && yarn format-check", - "lint": "yarn lint:css && yarn lint:js", - "lint:js": "eslint --cache src/**/*.{ts,tsx}", - "lint:css": "stylelint \"src/**/*.scss\"", - "format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"", - "format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"", + "build-ci": "yarn run validate && yarn run build", + "validate": "yarn run lint && yarn run check && yarn run format-check", + "lint": "yarn run lint:css && yarn run lint:js", + "lint:css": "stylelint --cache \"src/**/*.scss\"", + "lint:js": "eslint --cache src/", + "check": "tsc --noEmit", + "format": "prettier --write .", + "format-check": "prettier --check .", "gqlgen": "gql-gen --config codegen.yml", "extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'" }, "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" + ">0.5% and supports es6-module-dynamic-import" ], "dependencies": { - "@apollo/client": "^3.3.7", - "@formatjs/intl-getcanonicallocales": "^1.5.3", - "@formatjs/intl-locale": "^2.4.14", - "@formatjs/intl-numberformat": "^6.1.3", - "@formatjs/intl-pluralrules": "^4.0.6", - "@fortawesome/fontawesome-svg-core": "^1.2.34", - "@fortawesome/free-regular-svg-icons": "^5.15.2", - "@fortawesome/free-solid-svg-icons": "^5.15.2", - "@fortawesome/react-fontawesome": "^0.1.14", - "@types/react-select": "^4.0.8", - "ansi-regex": "^5.0.1", - "apollo-upload-client": "^14.1.3", - "axios": "^1.1.3", + "@ant-design/react-slick": "^1.0.0", + "@apollo/client": "^3.7.8", + "@formatjs/intl-getcanonicallocales": "^2.0.5", + "@formatjs/intl-locale": "^3.0.11", + "@formatjs/intl-numberformat": "^8.3.3", + "@formatjs/intl-pluralrules": "^5.1.8", + "@fortawesome/fontawesome-svg-core": "^6.3.0", + "@fortawesome/free-brands-svg-icons": "^6.3.0", + "@fortawesome/free-regular-svg-icons": "^6.3.0", + "@fortawesome/free-solid-svg-icons": "^6.3.0", + "@fortawesome/react-fontawesome": "^0.2.0", + "apollo-upload-client": "^17.0.0", + "axios": "^1.3.3", "base64-blob": "^1.4.1", - "bootstrap": "^4.6.0", - "classnames": "^2.2.6", - "flag-icon-css": "^3.5.0", + "bootstrap": "^4.6.2", + "classnames": "^2.3.2", + "flag-icons": "^6.6.6", "flexbin": "^0.2.0", - "formik": "^2.2.6", - "graphql": "^15.4.0", - "graphql-tag": "^2.11.0", - "i18n-iso-countries": "^6.4.0", - "intersection-observer": "^0.12.0", - "localforage": "^1.9.0", + "formik": "^2.2.9", + "graphql": "^16.6.0", + "graphql-tag": "^2.12.6", + "graphql-ws": "^5.11.3", + "i18n-iso-countries": "^7.5.0", + "intersection-observer": "^0.12.2", + "localforage": "^1.10.0", "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "mousetrap-pause": "^1.0.0", "normalize-url": "^4.5.1", - "postcss": "^8.2.10", - "query-string": "6.13.8", - "react": "17.0.2", - "react-bootstrap": "1.4.3", - "react-dom": "17.0.2", + "react": "^17.0.2", + "react-bootstrap": "^1.6.6", + "react-dom": "^17.0.2", "react-helmet": "^6.1.0", - "react-intl": "^5.10.16", - "react-markdown": "^7.1.0", + "react-intl": "^6.2.8", + "react-markdown": "^8.0.5", "react-router-bootstrap": "^0.25.0", - "react-router-dom": "^5.2.0", - "react-router-hash-link": "^2.3.1", - "react-select": "^4.0.2", - "react-slick": "^0.29.0", - "remark-gfm": "^1.0.0", + "react-router-dom": "^5.3.4", + "react-router-hash-link": "^2.4.3", + "react-select": "^5.7.0", + "remark-gfm": "^3.0.1", "resize-observer-polyfill": "^1.5.1", - "sass": "^1.32.5", "slick-carousel": "^1.8.1", - "string.prototype.replaceall": "^1.0.4", - "subscriptions-transport-ws": "^0.9.18", + "string.prototype.replaceall": "^1.0.7", "thehandy": "^1.0.3", "universal-cookie": "^4.0.4", - "video.js": "^7.20.3", + "video.js": "^7.21.1", "videojs-mobile-ui": "^0.8.0", "videojs-seek-buttons": "^3.0.1", "videojs-vtt.js": "^0.15.4", - "vite": "^2.9.13", - "vite-plugin-compression": "^0.3.5", - "vite-tsconfig-paths": "^3.3.17", - "ws": "^7.4.6", - "yup": "^0.32.9" + "yup": "^1.0.0" }, "devDependencies": { - "@graphql-codegen/add": "^2.0.2", - "@graphql-codegen/cli": "^1.20.0", - "@graphql-codegen/time": "^2.0.2", - "@graphql-codegen/typescript": "^1.20.00", - "@graphql-codegen/typescript-operations": "^1.17.13", - "@graphql-codegen/typescript-react-apollo": "^2.2.1", - "@types/apollo-upload-client": "^14.1.0", - "@types/classnames": "^2.2.11", - "@types/fslightbox-react": "^1.4.0", + "@babel/core": "^7.20.12", + "@graphql-codegen/cli": "^3.0.0", + "@graphql-codegen/time": "^4.0.0", + "@graphql-codegen/typescript": "^3.0.0", + "@graphql-codegen/typescript-operations": "^3.0.0", + "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@types/apollo-upload-client": "^17.0.2", "@types/lodash-es": "^4.17.6", - "@types/mousetrap": "^1.6.5", - "@types/node": "14.14.22", - "@types/react": "17.0.31", - "@types/react-dom": "^17.0.10", - "@types/react-helmet": "^6.1.3", + "@types/mousetrap": "^1.6.11", + "@types/node": "^18.13.0", + "@types/react": "^17.0.53", + "@types/react-dom": "^17.0.19", + "@types/react-helmet": "^6.1.6", "@types/react-router-bootstrap": "^0.24.5", - "@types/react-router-dom": "5.1.7", - "@types/react-router-hash-link": "^1.2.1", - "@types/react-slick": "^0.23.8", - "@types/video.js": "^7.3.49", + "@types/react-router-hash-link": "^2.4.5", + "@types/video.js": "^7.3.51", "@types/videojs-mobile-ui": "^0.5.0", "@types/videojs-seek-buttons": "^2.1.0", - "@typescript-eslint/eslint-plugin": "^4.33.0", - "@typescript-eslint/parser": "^4.33.0", - "eslint": "^7.32.0", - "eslint-config-airbnb": "^18.2.1", - "eslint-config-airbnb-typescript": "^14.0.1", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-react": "^7.26.1", - "eslint-plugin-react-hooks": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^5.52.0", + "@typescript-eslint/parser": "^5.52.0", + "@vitejs/plugin-react": "^3.1.0", + "eslint": "^8.34.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", "extract-react-intl-messages": "^4.1.1", - "postcss-safe-parser": "^5.0.2", - "prettier": "2.2.1", - "stylelint": "^13.9.0", - "stylelint-config-prettier": "^8.0.2", - "stylelint-order": "^4.1.0", - "typescript": "~4.4.4" + "postcss": "^8.4.21", + "postcss-scss": "^4.0.6", + "prettier": "^2.8.4", + "sass": "^1.58.1", + "stylelint": "^15.1.0", + "stylelint-order": "^6.0.2", + "ts-node": "^10.9.1", + "typescript": "~4.8.4", + "vite": "^4.1.1", + "vite-plugin-compression": "^0.5.1", + "vite-tsconfig-paths": "^4.0.5" } } diff --git a/ui/v2.5/src/@types/mousetrap-pause.d.ts b/ui/v2.5/src/@types/mousetrap-pause.d.ts new file mode 100644 index 000000000..8abd93fcb --- /dev/null +++ b/ui/v2.5/src/@types/mousetrap-pause.d.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +declare module "mousetrap-pause" { + import { MousetrapStatic } from "mousetrap"; + + function MousetrapPause(mousetrap: MousetrapStatic): MousetrapStatic; + + export default MousetrapPause; + + module "mousetrap" { + interface MousetrapStatic { + pause(): void; + unpause(): void; + pauseCombo(combo: string): void; + unpauseCombo(combo: string): void; + } + interface MousetrapInstance { + pause(): void; + unpause(): void; + pauseCombo(combo: string): void; + unpauseCombo(combo: string): void; + } + } +} diff --git a/ui/v2.5/src/@types/string.prototype.replaceall.d.ts b/ui/v2.5/src/@types/string.prototype.replaceall.d.ts new file mode 100644 index 000000000..fa87eec06 --- /dev/null +++ b/ui/v2.5/src/@types/string.prototype.replaceall.d.ts @@ -0,0 +1,19 @@ +declare module "string.prototype.replaceall" { + function replaceAll( + searchValue: string | RegExp, + replaceValue: string + ): string; + function replaceAll( + searchValue: string | RegExp, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + replacer: (substring: string, ...args: any[]) => string + ): string; + + namespace replaceAll { + function getPolyfill(): typeof replaceAll; + function implementation(): typeof replaceAll; + function shim(): void; + } + + export default replaceAll; +} diff --git a/ui/v2.5/src/@types/videojs-vtt.d.ts b/ui/v2.5/src/@types/videojs-vtt.d.ts index 7140e5b6e..191c0d27d 100644 --- a/ui/v2.5/src/@types/videojs-vtt.d.ts +++ b/ui/v2.5/src/@types/videojs-vtt.d.ts @@ -1,111 +1,111 @@ /* eslint-disable @typescript-eslint/naming-convention */ declare module "videojs-vtt.js" { - namespace vttjs { - /** - * A custom JS error object that is reported through the parser's `onparsingerror` callback. - * It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object. - * - * There are two error codes that can be reported back currently: - * * 0 BadSignature - * * 1 BadTimeStamp - * - * Note: Exceptions other then ParsingError will be thrown and not reported. - */ - class ParsingError extends Error { - readonly name: string; - readonly code: number; - readonly message: string; - } - - namespace WebVTT { - /** - * A parser for the WebVTT spec in JavaScript. - */ - class Parser { - /** - * The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions` - * as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives. - * For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`. - * If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec. - * - * @param window the window object to use - * @param vttjs the vtt.js module - * @param decoder the decoder to decode `parse()` data with - */ - constructor(window: Window); - constructor(window: Window, decoder: TextDecoder); - constructor(window: Window, vttjs: vttjs, decoder: TextDecoder); - - /** - * Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object. - */ - onregion?: (cue: VTTRegion) => void; - - /** - * Callback that is invoked for every cue that is fully parsed. In case of streaming parsing, - * `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object. - */ - oncue?: (cue: VTTCue) => void; - - /** - * Is invoked in response to `flush()` and after the content was parsed completely. - */ - onflush?: () => void; - - /** - * Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed. - * Is passed a `ParsingError` object. - */ - onparsingerror?: (e: ParsingError) => void; - - /** - * Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the - * StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks. - * - * @param data data to be parsed - */ - parse(data: string): this; - - /** - * Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have. - * Will also trigger `onflush`. - */ - flush(): this; - } - - /** - * Helper to allow strings to be decoded instead of the default binary utf8 data. - */ - function StringDecoder(): TextDecoder; - - /** - * Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text. - * It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div. - * - * @param window window object to use - * @param cuetext cue text to parse - */ - function convertCueToDOMTree( - window: Window, - cuetext: string - ): HTMLDivElement | null; - - /** - * Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the - * processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles - * to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay). - * The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance. - * - * @param overlay A block level element (usually a div) that the computed cues and regions will be placed into. - */ - function processCues( - window: Window, - cues: VTTCue[], - overlay: Element - ): void; - } + /** + * A custom JS error object that is reported through the parser's `onparsingerror` callback. + * It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object. + * + * There are two error codes that can be reported back currently: + * * 0 BadSignature + * * 1 BadTimeStamp + * + * Note: Exceptions other then ParsingError will be thrown and not reported. + */ + class ParsingError extends Error { + readonly name: string; + readonly code: number; + readonly message: string; } - export = vttjs; + export namespace WebVTT { + /** + * A parser for the WebVTT spec in JavaScript. + */ + class Parser { + /** + * The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions` + * as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives. + * For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`. + * If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec. + * + * @param window the window object to use + * @param vttjs the vtt.js module + * @param decoder the decoder to decode `parse()` data with + */ + constructor(window: Window); + constructor(window: Window, decoder: TextDecoder); + constructor( + window: Window, + vttjs: typeof import("videojs-vtt.js"), + decoder: TextDecoder + ); + + /** + * Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object. + */ + onregion?: (cue: VTTRegion) => void; + + /** + * Callback that is invoked for every cue that is fully parsed. In case of streaming parsing, + * `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object. + */ + oncue?: (cue: VTTCue) => void; + + /** + * Is invoked in response to `flush()` and after the content was parsed completely. + */ + onflush?: () => void; + + /** + * Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed. + * Is passed a `ParsingError` object. + */ + onparsingerror?: (e: ParsingError) => void; + + /** + * Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the + * StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks. + * + * @param data data to be parsed + */ + parse(data: string): this; + + /** + * Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have. + * Will also trigger `onflush`. + */ + flush(): this; + } + + /** + * Helper to allow strings to be decoded instead of the default binary utf8 data. + */ + function StringDecoder(): TextDecoder; + + /** + * Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text. + * It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div. + * + * @param window window object to use + * @param cuetext cue text to parse + */ + function convertCueToDOMTree( + window: Window, + cuetext: string + ): HTMLDivElement | null; + + /** + * Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the + * processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles + * to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay). + * The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance. + * + * @param overlay A block level element (usually a div) that the computed cues and regions will be placed into. + */ + function processCues( + window: Window, + cues: VTTCue[], + overlay: Element + ): void; + } } diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 71a0755e8..fc7925e22 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -5,7 +5,7 @@ import { Helmet } from "react-helmet"; import cloneDeep from "lodash-es/cloneDeep"; import mergeWith from "lodash-es/mergeWith"; import { ToastProvider } from "src/hooks/Toast"; -import LightboxProvider from "src/hooks/Lightbox/context"; +import { LightboxProvider } from "src/hooks/Lightbox/context"; import { initPolyfills } from "src/polyfills"; import locales, { registerCountry } from "src/locales"; @@ -14,14 +14,15 @@ import { useConfigureUI, useSystemStatus, } from "src/core/StashService"; -import { flattenMessages } from "src/utils"; +import flattenMessages from "./utils/flattenMessages"; import Mousetrap from "mousetrap"; import MousetrapPause from "mousetrap-pause"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; import * as GQL from "./core/generated-graphql"; -import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared"; +import { TITLE_SUFFIX } from "./components/Shared/constants"; +import { LoadingIndicator } from "./components/Shared/LoadingIndicator"; import { ConfigurationProvider } from "./hooks/Config"; import { ManualProvider } from "./components/Help/context"; @@ -91,10 +92,12 @@ export const App: React.FC = () => { const defaultMessages = (await locales[defaultMessageLanguage]()).default; const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); const chosenMessages = (await locales[messageLanguage]()).default; - const res = await fetch(getPlatformURL() + "customlocales"); let customMessages = {}; try { - customMessages = res.ok ? await res.json() : {}; + const res = await fetch(getPlatformURL() + "customlocales"); + if (res.ok) { + customMessages = await res.json(); + } } catch (err) { console.log(err); } diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index 506d995f4..c99ad0f16 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useChangelogStorage } from "src/hooks"; +import { useChangelogStorage } from "src/hooks/LocalForage"; import Version from "./Version"; import V010 from "src/docs/en/Changelog/v010.md"; import V011 from "src/docs/en/Changelog/v011.md"; @@ -26,9 +26,6 @@ import V0180 from "src/docs/en/Changelog/v0180.md"; import V0190 from "src/docs/en/Changelog/v0190.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; -// to avoid use of explicit any -type Module = typeof V010; - const Changelog: React.FC = () => { const [{ data, loading }, setOpenState] = useChangelogStorage(); @@ -55,7 +52,7 @@ const Changelog: React.FC = () => { interface IStashRelease { version: string; date?: string; - page: Module; + page: string; defaultOpen?: boolean; } diff --git a/ui/v2.5/src/components/Changelog/Version.tsx b/ui/v2.5/src/components/Changelog/Version.tsx index cd5b99442..bc2f75138 100644 --- a/ui/v2.5/src/components/Changelog/Version.tsx +++ b/ui/v2.5/src/components/Changelog/Version.tsx @@ -2,7 +2,7 @@ import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Card, Collapse } from "react-bootstrap"; import { FormattedDate, FormattedMessage } from "react-intl"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; interface IVersionProps { version: string; diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index f9eb805ce..4a0657efe 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect, useMemo } from "react"; import { Form, Button } from "react-bootstrap"; import { mutateMetadataGenerate } from "src/core/StashService"; -import { Modal, Icon } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "../Shared/Modal"; +import { Icon } from "src/components/Shared/Icon"; +import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { ConfigurationContext } from "src/hooks/Config"; import { Manual } from "../Help/Manual"; -import { withoutTypename } from "src/utils"; +import { withoutTypename } from "src/utils/data"; import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; import { SettingSection } from "../Settings/SettingSection"; import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; @@ -169,7 +170,7 @@ export const GenerateDialog: React.FC = ({ } return ( - = ({ /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx index f8c146fbb..ba027cd5c 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from "react"; import { Form, Button, Table } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import { Icon } from "src/components/Shared/Icon"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { @@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC = ({ allowSetDefault = true, defaultOptions, }) => { - const [localFieldOptions, setLocalFieldOptions] = useState< - GQL.IdentifyFieldOptions[] - >(); + const [localFieldOptions, setLocalFieldOptions] = + useState(); const [editField, setEditField] = useState(); useEffect(() => { diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index f5964ee97..7c5207f44 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -6,11 +6,13 @@ import { useConfigureDefaults, useListSceneScrapers, } from "src/core/StashService"; -import { Icon, Modal, OperationButton } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { Icon } from "src/components/Shared/Icon"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { OperationButton } from "src/components/Shared/OperationButton"; +import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; -import { withoutTypename } from "src/utils"; +import { withoutTypename } from "src/utils/data"; import { SCRAPER_PREFIX, STASH_BOX_PREFIX, @@ -202,9 +204,8 @@ export const IdentifyDialog: React.FC = ({ if (s.options) { const sourceOptions = withoutTypename(s.options); - sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map( - withoutTypename - ); + sourceOptions.fieldOptions = + sourceOptions.fieldOptions?.map(withoutTypename); ret.options = sourceOptions; } @@ -215,9 +216,8 @@ export const IdentifyDialog: React.FC = ({ setSources(mappedSources); if (identifyDefaults.options) { const defaultOptions = withoutTypename(identifyDefaults.options); - defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map( - withoutTypename - ); + defaultOptions.fieldOptions = + defaultOptions.fieldOptions?.map(withoutTypename); setOptions(defaultOptions); } } else { @@ -405,7 +405,7 @@ export const IdentifyDialog: React.FC = ({ } return ( - = ({ setEditingField={(v) => setEditingField(v)} /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx index 9cc0c6a51..b18e661bc 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import { Form, Button, ListGroup } from "react-bootstrap"; -import { Modal, Icon } from "src/components/Shared"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { Icon } from "src/components/Shared/Icon"; import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { IScraperSource } from "./constants"; @@ -53,7 +54,7 @@ export const SourcesEditor: React.FC = ({ } return ( - = ({ defaultOptions={defaultOptions} /> - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts b/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts index 46ed88854..13889d037 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/constants.ts @@ -20,7 +20,7 @@ export const sceneFields = [ "tags", "stash_ids", ] as const; -export type SceneField = typeof sceneFields[number]; +export type SceneField = (typeof sceneFields)[number]; export const multiValueSceneFields: SceneField[] = [ "studio", diff --git a/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx b/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx index fefc344be..860e7a62d 100644 --- a/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ReleaseNotesDialog.tsx @@ -1,13 +1,12 @@ import React from "react"; import { Form } from "react-bootstrap"; -import { Modal } from "src/components/Shared"; +import { ModalComponent } from "../Shared/Modal"; import { faCogs } from "@fortawesome/free-solid-svg-icons"; import { useIntl } from "react-intl"; import { MarkdownPage } from "../Shared/MarkdownPage"; -import { Module } from "src/docs/en/ReleaseNotes"; interface IReleaseNotesDialog { - notes: Module[]; + notes: string[]; onClose: () => void; } @@ -18,7 +17,7 @@ export const ReleaseNotesDialog: React.FC = ({ const intl = useIntl(); return ( - = ({ ))} - + ); }; diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index dca73532f..ac0e99937 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { useMutation, DocumentNode } from "@apollo/client"; import { Button, Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { getStashboxBase } from "src/utils"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { getStashboxBase } from "src/utils/stashbox"; import { FormattedMessage, useIntl } from "react-intl"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; @@ -78,7 +78,7 @@ export const SubmitStashBoxDraft: React.FC = ({ undefined; return ( - = ({
{error.message}
)} -
+ ); }; diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx index d68e2ca00..856afb48b 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPage.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useConfigureUI } from "src/core/StashService"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button } from "react-bootstrap"; import { FrontPageConfig } from "./FrontPageConfig"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { Control } from "./Control"; import { ConfigurationContext } from "src/hooks/Config"; import { diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx index 4bbf6a7c0..39668abc5 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import { useFindSavedFilters } from "src/core/StashService"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button, Form, Modal } from "react-bootstrap"; import { FilterMode, diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 4e128dd28..fe9de2d97 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { useGalleryDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -119,7 +119,7 @@ export const DeleteGalleriesDialog: React.FC = ( } return ( - = ( onChange={() => setDeleteGenerated(!deleteGenerated)} /> - + ); }; diff --git a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx index 24e153acd..68fb1f310 100644 --- a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx @@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl"; import isEqual from "lodash-es/isEqual"; import { useBulkGalleryUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { StudioSelect, Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; -import MultiSet from "../Shared/MultiSet"; +import { StudioSelect } from "../Shared/Select"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; +import { MultiSet } from "../Shared/MultiSet"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputIDs, @@ -31,10 +32,8 @@ export const EditGalleriesDialog: React.FC = ( const Toast = useToast(); const [rating100, setRating] = useState(); const [studioId, setStudioId] = useState(); - const [ - performerMode, - setPerformerMode, - ] = React.useState(GQL.BulkUpdateIdMode.Add); + const [performerMode, setPerformerMode] = + React.useState(GQL.BulkUpdateIdMode.Add); const [performerIds, setPerformerIds] = useState(); const [existingPerformerIds, setExistingPerformerIds] = useState(); const [tagMode, setTagMode] = React.useState( @@ -228,7 +227,7 @@ export const EditGalleriesDialog: React.FC = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Galleries/Galleries.tsx b/ui/v2.5/src/components/Galleries/Galleries.tsx index 7dd4d3b97..8f4591ac0 100644 --- a/ui/v2.5/src/components/Galleries/Galleries.tsx +++ b/ui/v2.5/src/components/Galleries/Galleries.tsx @@ -2,13 +2,13 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "../Shared/constants"; import { PersistanceLevel } from "src/hooks/ListHook"; import Gallery from "./GalleryDetails/Gallery"; import GalleryCreate from "./GalleryDetails/GalleryCreate"; import { GalleryList } from "./GalleryList"; -const Galleries = () => { +const Galleries: React.FC = () => { const intl = useIntl(); const title_template = `${intl.formatMessage({ diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index b17b1cbf3..88fe37f2a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -2,17 +2,15 @@ import { Button, ButtonGroup } from "react-bootstrap"; import React from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { - GridCard, - HoverPopover, - Icon, - TagLink, - TruncatedText, -} from "src/components/Shared"; -import { PopoverCountButton } from "src/components/Shared/PopoverCountButton"; -import { NavUtils } from "src/utils"; -import { ConfigurationContext } from "src/hooks/Config"; +import { GridCard } from "../Shared/GridCard"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; +import { TruncatedText } from "../Shared/TruncatedText"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; +import { PopoverCountButton } from "../Shared/PopoverCountButton"; +import NavUtils from "src/utils/navigation"; +import { ConfigurationContext } from "src/hooks/Config"; import { RatingBanner } from "../Shared/RatingBanner"; import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 422e45d4e..dc508ad15 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -9,14 +9,12 @@ import { useFindGallery, useGalleryUpdate, } from "src/core/StashService"; -import { - ErrorMessage, - LoadingIndicator, - Icon, - Counter, -} from "src/components/Shared"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { Icon } from "src/components/Shared/Icon"; +import { Counter } from "src/components/Shared/Counter"; import Mousetrap from "mousetrap"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton"; import { GalleryEditPanel } from "./GalleryEditPanel"; import { GalleryDetailPanel } from "./GalleryDetailPanel"; @@ -214,7 +212,6 @@ export const GalleryPage: React.FC = ({ gallery }) => { setIsDeleteAlertOpen(true)} /> diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index 04a9db305..4f54ac445 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { ImageList } from "src/components/Images/ImageList"; import { showWhenSelected } from "src/hooks/ListHook"; import { mutateAddGalleryImages } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { useIntl } from "react-intl"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx index 09ad8d17f..62e80e23e 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryCreate.tsx @@ -1,18 +1,15 @@ -import React from "react"; +import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useLocation } from "react-router-dom"; import { GalleryEditPanel } from "./GalleryEditPanel"; const GalleryCreate: React.FC = () => { const intl = useIntl(); - - function useQuery() { - const { search } = useLocation(); - return React.useMemo(() => new URLSearchParams(search), [search]); - } - - const query = useQuery(); - const nameQuery = query.get("name"); + const location = useLocation(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const gallery = { + title: query.get("q") ?? undefined, + }; return (
@@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => { values={{ entityType: intl.formatMessage({ id: "gallery" }) }} /> - {}} - /> + {}} />
); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index c2cfece58..463ced506 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -2,8 +2,9 @@ import React from "react"; import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils } from "src/utils"; -import { TagLink, TruncatedText } from "src/components/Shared"; +import TextUtils from "src/utils/text"; +import { TagLink } from "src/components/Shared/TagLink"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { sortPerformers } from "src/core/performers"; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index 6e472412d..575653a3a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -25,13 +25,13 @@ import { TagSelect, SceneSelect, StudioSelect, - Icon, - LoadingIndicator, - URLField, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +} from "src/components/Shared/Select"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; import { useFormik } from "formik"; -import { FormUtils } from "src/utils"; +import FormUtils from "src/utils/form"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; @@ -40,23 +40,16 @@ import { useRatingKeybinds } from "src/hooks/keybinds"; import { ConfigurationContext } from "src/hooks/Config"; interface IProps { + gallery: Partial; isVisible: boolean; onDelete: () => void; } -interface INewProps { - isNew: true; - gallery?: Partial; -} - -interface IExistingProps { - isNew: false; - gallery: GQL.GalleryDataFragment; -} - -export const GalleryEditPanel: React.FC< - IProps & (INewProps | IExistingProps) -> = ({ gallery, isNew, isVisible, onDelete }) => { +export const GalleryEditPanel: React.FC = ({ + gallery, + isVisible, + onDelete, +}) => { const intl = useIntl(); const Toast = useToast(); const history = useHistory(); @@ -67,15 +60,14 @@ export const GalleryEditPanel: React.FC< })) ); + const isNew = gallery.id === undefined; const { configuration: stashConfig } = React.useContext(ConfigurationContext); const Scrapers = useListGalleryScrapers(); const [queryableScrapers, setQueryableScrapers] = useState([]); - const [ - scrapedGallery, - setScrapedGallery, - ] = useState(); + const [scrapedGallery, setScrapedGallery] = + useState(); // Network state const [isLoading, setIsLoading] = useState(false); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx index 5fd71c1ac..1ce1ac825 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx @@ -1,12 +1,12 @@ import React, { useMemo, useState } from "react"; import { Accordion, Button, Card } from "react-bootstrap"; import { FormattedMessage, FormattedTime } from "react-intl"; -import { TruncatedText } from "src/components/Shared"; -import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; +import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog"; import * as GQL from "src/core/generated-graphql"; import { mutateGallerySetPrimaryFile } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { TextUtils } from "src/utils"; +import { useToast } from "src/hooks/Toast"; +import TextUtils from "src/utils/text"; import { TextField, URLField } from "src/utils/field"; interface IFileInfoPanelProps { diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx index bddd55da5..69fd7a6ee 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx @@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter"; import { ImageList } from "src/components/Images/ImageList"; import { mutateRemoveGalleryImages } from "src/core/StashService"; import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { useIntl } from "react-intl"; import { faMinus } from "@fortawesome/free-solid-svg-icons"; import { galleryTitle } from "src/core/galleries"; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index 917e72b28..153510fd8 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -1,8 +1,11 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { StudioSelect, PerformerSelect } from "src/components/Shared"; +import { + StudioSelect, + PerformerSelect, + TagSelect, +} from "src/components/Shared/Select"; import * as GQL from "src/core/generated-graphql"; -import { TagSelect } from "src/components/Shared/Select"; import { ScrapeDialog, ScrapeDialogRow, @@ -17,7 +20,7 @@ import { useTagCreate, makePerformerCreateInput, } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; function renderScrapedStudio( result: ScrapeResult, diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index ab33cff3e..d2ba17d6f 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -8,8 +8,11 @@ import { FindGalleriesQueryResult, SlimGalleryDataFragment, } from "src/core/generated-graphql"; -import { useGalleriesList } from "src/hooks"; -import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; +import { + showWhenSelected, + PersistanceLevel, + useGalleriesList, +} from "src/hooks/ListHook"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { queryFindGalleries } from "src/core/StashService"; diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index 437eaeb94..dbd10e090 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -1,6 +1,6 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { useFindGalleries } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { GalleryCard } from "./GalleryCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; @@ -13,9 +13,7 @@ interface IProps { header: string; } -export const GalleryRecommendationRow: FunctionComponent = ( - props: IProps -) => { +export const GalleryRecommendationRow: React.FC = (props) => { const result = useFindGalleries(props.filter); const cardCount = result.data?.findGalleries.count; diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 868f63dc1..67736c50d 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; -import { useLightbox } from "src/hooks"; -import { LoadingIndicator } from "src/components/Shared"; +import { useLightbox } from "src/hooks/Lightbox/hooks"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import "flexbin/flexbin.css"; import { CriterionModifier, diff --git a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx index a357b6722..b5e1ff7ec 100644 --- a/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryWallCard.tsx @@ -2,9 +2,9 @@ import React from "react"; import { useIntl } from "react-intl"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { TruncatedText } from "src/components/Shared"; -import { TextUtils } from "src/utils"; -import { useGalleryLightbox } from "src/hooks"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; +import TextUtils from "src/utils/text"; +import { useGalleryLightbox } from "src/hooks/Lightbox/hooks"; import { galleryTitle } from "src/core/galleries"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; diff --git a/ui/v2.5/src/components/Galleries/styles.scss b/ui/v2.5/src/components/Galleries/styles.scss index 5e9adce98..1ee4ee152 100644 --- a/ui/v2.5/src/components/Galleries/styles.scss +++ b/ui/v2.5/src/components/Galleries/styles.scss @@ -1,3 +1,5 @@ +@use "sass:math"; + .gallery-image { &:hover { cursor: pointer; @@ -138,14 +140,14 @@ $galleryTabWidth: 450px; } @mixin galleryWidth($width) { - height: ($width / 3) * 2; + height: math.div($width, 3) * 2; &-landscape { width: $width; } &-portrait { - width: $width / 2; + width: math.div($width, 2); } } @@ -216,9 +218,17 @@ $galleryTabWidth: 450px; display: none; } + .star-fill-10 .unfilled-star, + .star-fill-20 .unfilled-star, .star-fill-25 .unfilled-star, + .star-fill-30 .unfilled-star, + .star-fill-40 .unfilled-star, .star-fill-50 .unfilled-star, - .star-fill-75 .unfilled-star { + .star-fill-60 .unfilled-star, + .star-fill-70 .unfilled-star, + .star-fill-75 .unfilled-star, + .star-fill-80 .unfilled-star, + .star-fill-90 .unfilled-star { visibility: hidden; } diff --git a/ui/v2.5/src/components/Help/Manual.tsx b/ui/v2.5/src/components/Help/Manual.tsx index 12faa3e26..7af8756f8 100644 --- a/ui/v2.5/src/components/Help/Manual.tsx +++ b/ui/v2.5/src/components/Help/Manual.tsx @@ -173,11 +173,9 @@ export const Manual: React.FC = ({ event: React.MouseEvent ) { if (event.target instanceof HTMLAnchorElement) { - const href = (event.target as HTMLAnchorElement).getAttribute("href"); + const href = event.target.getAttribute("href"); if (href && href.startsWith("/help")) { - const newKey = (event.target as HTMLAnchorElement).pathname.substring( - "/help/".length - ); + const newKey = event.target.pathname.substring("/help/".length); setActiveTab(newKey); event.preventDefault(); } diff --git a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx index 86f2ddbb8..31fa587f7 100644 --- a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx @@ -2,8 +2,8 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { useImagesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -112,7 +112,7 @@ export const DeleteImagesDialog: React.FC = ( } return ( - = ( onChange={() => setDeleteGenerated(!deleteGenerated)} /> - + ); }; diff --git a/ui/v2.5/src/components/Images/EditImagesDialog.tsx b/ui/v2.5/src/components/Images/EditImagesDialog.tsx index 6d18f9df3..86bb87abc 100644 --- a/ui/v2.5/src/components/Images/EditImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/EditImagesDialog.tsx @@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl"; import isEqual from "lodash-es/isEqual"; import { useBulkImageUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { StudioSelect, Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; -import MultiSet from "../Shared/MultiSet"; +import { StudioSelect } from "src/components/Shared/Select"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; +import { MultiSet } from "../Shared/MultiSet"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputIDs, @@ -31,10 +32,8 @@ export const EditImagesDialog: React.FC = ( const Toast = useToast(); const [rating100, setRating] = useState(); const [studioId, setStudioId] = useState(); - const [ - performerMode, - setPerformerMode, - ] = React.useState(GQL.BulkUpdateIdMode.Add); + const [performerMode, setPerformerMode] = + React.useState(GQL.BulkUpdateIdMode.Add); const [performerIds, setPerformerIds] = useState(); const [existingPerformerIds, setExistingPerformerIds] = useState(); const [tagMode, setTagMode] = React.useState( @@ -218,7 +217,7 @@ export const EditImagesDialog: React.FC = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index bbaa14134..50ae8bcc4 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -2,10 +2,13 @@ import React, { MouseEvent, useMemo } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; -import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared"; -import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; -import { GridCard } from "../Shared/GridCard"; -import { RatingBanner } from "../Shared/RatingBanner"; +import { Icon } from "src/components/Shared/Icon"; +import { TagLink } from "src/components/Shared/TagLink"; +import { HoverPopover } from "src/components/Shared/HoverPopover"; +import { SweatDrops } from "src/components/Shared/SweatDrops"; +import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; +import { GridCard } from "src/components/Shared/GridCard"; +import { RatingBanner } from "src/components/Shared/RatingBanner"; import { faBox, faImages, diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 72d88bad9..eb3d1211c 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -11,13 +11,11 @@ import { useImageUpdate, mutateMetadataScan, } from "src/core/StashService"; -import { - ErrorMessage, - LoadingIndicator, - Icon, - Counter, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { Icon } from "src/components/Shared/Icon"; +import { Counter } from "src/components/Shared/Counter"; +import { useToast } from "src/hooks/Toast"; import * as Mousetrap from "mousetrap"; import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton"; import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton"; @@ -239,7 +237,9 @@ export const Image: React.FC = () => { Mousetrap.bind("a", () => setActiveTabKey("image-details-panel")); Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel")); Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel")); - Mousetrap.bind("o", () => onIncrementClick()); + Mousetrap.bind("o", () => { + onIncrementClick(); + }); return () => { Mousetrap.unbind("a"); diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx index 7d7489068..c4e840e2c 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx @@ -1,8 +1,9 @@ import React, { useMemo } from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils } from "src/utils"; -import { TagLink, TruncatedText } from "src/components/Shared"; +import TextUtils from "src/utils/text"; +import { TagLink } from "src/components/Shared/TagLink"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { sortPerformers } from "src/core/performers"; diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx index b21c84e00..b299c830f 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageEditPanel.tsx @@ -9,11 +9,11 @@ import { PerformerSelect, TagSelect, StudioSelect, - LoadingIndicator, - URLField, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { FormUtils } from "src/utils"; +} from "src/components/Shared/Select"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; +import FormUtils from "src/utils/form"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx index 85f0cf58b..026c51dea 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; import { Accordion, Button, Card } from "react-bootstrap"; import { FormattedMessage, FormattedNumber, FormattedTime } from "react-intl"; -import { TruncatedText } from "src/components/Shared"; -import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog"; +import { TruncatedText } from "src/components/Shared/TruncatedText"; +import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog"; import * as GQL from "src/core/generated-graphql"; import { mutateImageSetPrimaryFile } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { TextUtils } from "src/utils"; +import { useToast } from "src/hooks/Toast"; +import TextUtils from "src/utils/text"; import { TextField, URLField } from "src/utils/field"; interface IFileInfoPanelProps { diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index 76b0ccf32..99103f521 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -9,13 +9,14 @@ import { } from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql"; import { queryFindImages } from "src/core/StashService"; -import { useImagesList, useLightbox } from "src/hooks"; +import { useLightbox } from "src/hooks/Lightbox/hooks"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; import { IListHookOperation, showWhenSelected, PersistanceLevel, + useImagesList, } from "src/hooks/ListHook"; import { ImageCard } from "./ImageCard"; diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx index 55f7e7b78..36f13b8d4 100644 --- a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -1,6 +1,6 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { useFindImages } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; import { RecommendationRow } from "../FrontPage/RecommendationRow"; @@ -13,9 +13,7 @@ interface IProps { header: string; } -export const ImageRecommendationRow: FunctionComponent = ( - props: IProps -) => { +export const ImageRecommendationRow: React.FC = (props: IProps) => { const result = useFindImages(props.filter); const cardCount = result.data?.findImages.count; diff --git a/ui/v2.5/src/components/Images/Images.tsx b/ui/v2.5/src/components/Images/Images.tsx index be16ed0b6..29538a6bb 100644 --- a/ui/v2.5/src/components/Images/Images.tsx +++ b/ui/v2.5/src/components/Images/Images.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "../Shared/constants"; import { PersistanceLevel } from "src/hooks/ListHook"; import { Image } from "./ImageDetails/Image"; import { ImageList } from "./ImageList"; diff --git a/ui/v2.5/src/components/List/AddFilterDialog.tsx b/ui/v2.5/src/components/List/AddFilterDialog.tsx index bad1a9dd9..c7c1e43c5 100644 --- a/ui/v2.5/src/components/List/AddFilterDialog.tsx +++ b/ui/v2.5/src/components/List/AddFilterDialog.tsx @@ -36,7 +36,7 @@ import { InputFilter } from "./Filters/InputFilter"; import { DateFilter } from "./Filters/DateFilter"; import { TimestampFilter } from "./Filters/TimestampFilter"; import { CountryCriterion } from "src/models/list-filter/criteria/country"; -import { CountrySelect } from "../Shared"; +import { CountrySelect } from "../Shared/CountrySelect"; import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids"; import { StashIDFilter } from "./Filters/StashIDFilter"; import { ConfigurationContext } from "src/hooks/Config"; diff --git a/ui/v2.5/src/components/List/FilterTags.tsx b/ui/v2.5/src/components/List/FilterTags.tsx index 48e79da98..f1eb02283 100644 --- a/ui/v2.5/src/components/List/FilterTags.tsx +++ b/ui/v2.5/src/components/List/FilterTags.tsx @@ -5,7 +5,7 @@ import { CriterionValue, } from "src/models/list-filter/criteria/criterion"; import { useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faTimes } from "@fortawesome/free-solid-svg-icons"; interface IFilterTagsProps { diff --git a/ui/v2.5/src/components/List/Filters/DurationFilter.tsx b/ui/v2.5/src/components/List/Filters/DurationFilter.tsx index 3fffa954a..772bb0137 100644 --- a/ui/v2.5/src/components/List/Filters/DurationFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/DurationFilter.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Form } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { CriterionModifier } from "../../../core/generated-graphql"; -import { DurationInput } from "../../Shared"; -import { INumberValue } from "../../../models/list-filter/types"; -import { Criterion } from "../../../models/list-filter/criteria/criterion"; +import { CriterionModifier } from "src/core/generated-graphql"; +import { DurationInput } from "src/components/Shared/DurationInput"; +import { INumberValue } from "src/models/list-filter/types"; +import { Criterion } from "src/models/list-filter/criteria/criterion"; interface IDurationFilterProps { criterion: Criterion; diff --git a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx index 330a756b9..8ea26bae5 100644 --- a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx @@ -10,10 +10,9 @@ interface IHierarchicalLabelValueFilterProps { onValueChanged: (value: IHierarchicalLabelValue) => void; } -export const HierarchicalLabelValueFilter: React.FC = ({ - criterion, - onValueChanged, -}) => { +export const HierarchicalLabelValueFilter: React.FC< + IHierarchicalLabelValueFilterProps +> = ({ criterion, onValueChanged }) => { const intl = useIntl(); if ( diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 51e1acd7d..646bb9a28 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -17,9 +17,9 @@ import { Overlay, } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import { Icon } from "../Shared/Icon"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { useFocus } from "src/utils"; +import useFocus from "src/utils/focus"; import { ListFilterOptions } from "src/models/list-filter/filter-options"; import { FormattedMessage, useIntl } from "react-intl"; import { PersistanceLevel } from "src/hooks/ListHook"; diff --git a/ui/v2.5/src/components/List/ListOperationButtons.tsx b/ui/v2.5/src/components/List/ListOperationButtons.tsx index 15a94b75e..c279020e9 100644 --- a/ui/v2.5/src/components/List/ListOperationButtons.tsx +++ b/ui/v2.5/src/components/List/ListOperationButtons.tsx @@ -9,7 +9,7 @@ import { import Mousetrap from "mousetrap"; import { FormattedMessage, useIntl } from "react-intl"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faEllipsisH, faPencilAlt, diff --git a/ui/v2.5/src/components/List/ListViewOptions.tsx b/ui/v2.5/src/components/List/ListViewOptions.tsx index ec31cf452..2dc84d09a 100644 --- a/ui/v2.5/src/components/List/ListViewOptions.tsx +++ b/ui/v2.5/src/components/List/ListViewOptions.tsx @@ -9,7 +9,7 @@ import { } from "react-bootstrap"; import { DisplayMode } from "src/models/list-filter/types"; import { useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faList, faSquare, diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index 87be25365..0f0d497be 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -15,13 +15,13 @@ import { useSaveFilter, useSetDefaultFilter, } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; import { ListFilterModel } from "src/models/list-filter/filter"; import { SavedFilterDataFragment } from "src/core/generated-graphql"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { PersistanceLevel } from "src/hooks/ListHook"; import { FormattedMessage, useIntl } from "react-intl"; -import { Icon } from "../Shared"; +import { Icon } from "../Shared/Icon"; import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons"; interface ISavedFilterListProps { diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index d48fb015a..85eea19c0 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -11,14 +11,14 @@ import { LinkContainer } from "react-router-bootstrap"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { SessionUtils } from "src/utils"; -import Icon from "src/components/Shared/Icon"; +import SessionUtils from "src/utils/session"; +import { Icon } from "src/components/Shared/Icon"; import { ConfigurationContext } from "src/hooks/Config"; import { ManualStateContext } from "./Help/context"; import { SettingsButton } from "./SettingsButton"; import { faBars, - faChartBar, + faChartColumn, faFilm, faHeart, faImage, @@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => { const pathname = location.pathname.replace(/\/$/, ""); let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null; - if (newPath != null) { + if (newPath !== null) { let queryParam = new URLSearchParams(location.search).get("q"); - if (queryParam != null) { - newPath += "?name=" + encodeURIComponent(queryParam); + if (queryParam) { + newPath += "?q=" + encodeURIComponent(queryParam); } } @@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => { className="minimal d-flex align-items-center h-100" title={intl.formatMessage({ id: "statistics" })} > - + = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index 5c766c97b..dc1087264 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -1,13 +1,11 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { - GridCard, - HoverPopover, - Icon, - TagLink, - TruncatedText, -} from "src/components/Shared"; +import { GridCard } from "../Shared/GridCard"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; +import { TruncatedText } from "../Shared/TruncatedText"; import { FormattedMessage } from "react-intl"; import { RatingBanner } from "../Shared/RatingBanner"; import { faPlayCircle } from "@fortawesome/free-solid-svg-icons"; @@ -20,7 +18,7 @@ interface IProps { onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } -export const MovieCard: FunctionComponent = (props: IProps) => { +export const MovieCard: React.FC = (props: IProps) => { function maybeRenderSceneNumber() { if (!props.sceneIndex) return; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 3984d4c33..87bfb42eb 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -9,13 +9,11 @@ import { useMovieDestroy, } from "src/core/StashService"; import { useParams, useHistory } from "react-router-dom"; -import { - DetailsEditNavbar, - ErrorMessage, - LoadingIndicator, - Modal, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { useToast } from "src/hooks/Toast"; import { MovieScenesPanel } from "./MovieScenesPanel"; import { MovieDetailsPanel } from "./MovieDetailsPanel"; import { MovieEditPanel } from "./MovieEditPanel"; @@ -51,7 +49,9 @@ const MoviePage: React.FC = ({ movie }) => { // set up hotkeys useEffect(() => { Mousetrap.bind("e", () => setIsEditing(true)); - Mousetrap.bind("d d", () => onDelete()); + Mousetrap.bind("d d", () => { + onDelete(); + }); return () => { Mousetrap.unbind("e"); @@ -109,7 +109,7 @@ const MoviePage: React.FC = ({ movie }) => { function renderDeleteAlert() { return ( - = ({ movie }) => { }} />

-
+ ); } diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx index faef1c20c..c34f43f89 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieCreate.tsx @@ -1,22 +1,24 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { useMovieCreate } from "src/core/StashService"; -import { useHistory } from "react-router-dom"; -import { LoadingIndicator } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { useHistory, useLocation } from "react-router-dom"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { useToast } from "src/hooks/Toast"; import { MovieEditPanel } from "./MovieEditPanel"; const MovieCreate: React.FC = () => { const history = useHistory(); + const location = useLocation(); const Toast = useToast(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const movie = { + name: query.get("q") ?? undefined, + }; + // Editing movie state - const [frontImage, setFrontImage] = useState( - undefined - ); - const [backImage, setBackImage] = useState( - undefined - ); + const [frontImage, setFrontImage] = useState(); + const [backImage, setBackImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); const [createMovie] = useMovieCreate(); @@ -84,6 +86,7 @@ const MovieCreate: React.FC = () => { history.push("/movies")} onDelete={() => {}} diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx index 2c4851bf6..bccbe36b6 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx @@ -1,7 +1,8 @@ import React from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { DurationUtils, TextUtils } from "src/utils"; +import DurationUtils from "src/utils/duration"; +import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { TextField, URLField } from "src/utils/field"; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index b822724b5..83935a256 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -7,16 +7,16 @@ import { queryScrapeMovieURL, useListMovieScrapers, } from "src/core/StashService"; -import { - LoadingIndicator, - StudioSelect, - DetailsEditNavbar, - DurationInput, - URLField, -} from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { StudioSelect } from "src/components/Shared/Select"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { DurationInput } from "src/components/Shared/DurationInput"; +import { URLField } from "src/components/Shared/URLField"; +import { useToast } from "src/hooks/Toast"; import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap"; -import { DurationUtils, FormUtils, ImageUtils } from "src/utils"; +import DurationUtils from "src/utils/duration"; +import FormUtils from "src/utils/form"; +import ImageUtils from "src/utils/image"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; @@ -25,7 +25,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds"; import { ConfigurationContext } from "src/hooks/Config"; interface IMovieEditPanel { - movie?: Partial; + movie: Partial; onSubmit: ( movie: Partial ) => void; @@ -49,19 +49,15 @@ export const MovieEditPanel: React.FC = ({ const Toast = useToast(); const { configuration: stashConfig } = React.useContext(ConfigurationContext); - const isNew = movie === undefined; + const isNew = movie.id === undefined; const [isLoading, setIsLoading] = useState(false); const [isImageAlertOpen, setIsImageAlertOpen] = useState(false); - const [imageClipboard, setImageClipboard] = useState( - undefined - ); + const [imageClipboard, setImageClipboard] = useState(); const Scrapers = useListMovieScrapers(); - const [scrapedMovie, setScrapedMovie] = useState< - GQL.ScrapedMovie | undefined - >(); + const [scrapedMovie, setScrapedMovie] = useState(); const schema = yup.object({ name: yup.string().required(), @@ -113,10 +109,10 @@ export const MovieEditPanel: React.FC = ({ setBackImage(formik.values.back_image); }, [formik.values.back_image, setBackImage]); - useEffect(() => onImageEncoding(encodingImage), [ - onImageEncoding, - encodingImage, - ]); + useEffect( + () => onImageEncoding(encodingImage), + [onImageEncoding, encodingImage] + ); function setRating(v: number) { formik.setFieldValue("rating100", v); diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx index 08c545e04..a6f53f179 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScrapeDialog.tsx @@ -9,10 +9,10 @@ import { ScrapeDialogRow, ScrapedTextAreaRow, } from "src/components/Shared/ScrapeDialog"; -import { StudioSelect } from "src/components/Shared"; -import { DurationUtils } from "src/utils"; +import { StudioSelect } from "src/components/Shared/Select"; +import DurationUtils from "src/utils/duration"; import { useStudioCreate } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import { useToast } from "src/hooks/Toast"; function renderScrapedStudio( result: ScrapeResult, diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx index 51a6bd2b0..ec367d74f 100644 --- a/ui/v2.5/src/components/Movies/MovieList.tsx +++ b/ui/v2.5/src/components/Movies/MovieList.tsx @@ -16,7 +16,8 @@ import { useMoviesList, PersistanceLevel, } from "src/hooks/ListHook"; -import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; +import { ExportDialog } from "../Shared/ExportDialog"; +import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { MovieCard } from "./MovieCard"; import { EditMoviesDialog } from "./EditMoviesDialog"; diff --git a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx index 710b54b59..34d5119ff 100644 --- a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx +++ b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useFindMovies } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { MovieCard } from "./MovieCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; diff --git a/ui/v2.5/src/components/Movies/Movies.tsx b/ui/v2.5/src/components/Movies/Movies.tsx index af9b501b3..2c35759fb 100644 --- a/ui/v2.5/src/components/Movies/Movies.tsx +++ b/ui/v2.5/src/components/Movies/Movies.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; import Movie from "./MovieDetails/Movie"; import MovieCreate from "./MovieDetails/MovieCreate"; import { MovieList } from "./MovieList"; diff --git a/ui/v2.5/src/components/PageNotFound.tsx b/ui/v2.5/src/components/PageNotFound.tsx index 645709fcf..67cb6b38c 100644 --- a/ui/v2.5/src/components/PageNotFound.tsx +++ b/ui/v2.5/src/components/PageNotFound.tsx @@ -1,5 +1,5 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; -export const PageNotFound: FunctionComponent = () => { +export const PageNotFound: React.FC = () => { return

Page not found.

; }; diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index fc189eeb7..5f02351a0 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -3,9 +3,9 @@ import { Col, Form, Row } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { useBulkPerformerUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import MultiSet from "../Shared/MultiSet"; +import { ModalComponent } from "../Shared/Modal"; +import { useToast } from "src/hooks/Toast"; +import { MultiSet } from "../Shared/MultiSet"; import { RatingSystem } from "../Shared/Rating/RatingSystem"; import { getAggregateInputValue, @@ -20,7 +20,7 @@ import { import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox"; import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; -import { FormUtils } from "../../utils"; +import FormUtils from "src/utils/form"; interface IListOperationProps { selected: GQL.SlimPerformerDataFragment[]; @@ -60,10 +60,8 @@ export const EditPerformersDialog: React.FC = ( mode: GQL.BulkUpdateIdMode.Add, }); const [existingTagIds, setExistingTagIds] = useState(); - const [ - aggregateState, - setAggregateState, - ] = useState({}); + const [aggregateState, setAggregateState] = + useState({}); // weight needs conversion to/from number const [weight, setWeight] = useState(); const [updateInput, setUpdateInput] = useState( @@ -183,7 +181,7 @@ export const EditPerformersDialog: React.FC = ( function render() { return ( - = ( /> - + ); } diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 36189154d..f11f96dbe 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -2,14 +2,13 @@ import React from "react"; import { Link } from "react-router-dom"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { NavUtils, TextUtils } from "src/utils"; -import { - GridCard, - CountryFlag, - HoverPopover, - Icon, - TagLink, -} from "src/components/Shared"; +import NavUtils from "src/utils/navigation"; +import TextUtils from "src/utils/text"; +import { GridCard } from "../Shared/GridCard"; +import { CountryFlag } from "../Shared/CountryFlag"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; import { Button, ButtonGroup } from "react-bootstrap"; import { Criterion, @@ -19,6 +18,8 @@ import { PopoverCountButton } from "../Shared/PopoverCountButton"; import GenderIcon from "./GenderIcon"; import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons"; import { RatingBanner } from "../Shared/RatingBanner"; +import cx from "classnames"; +import { usePerformerUpdate } from "src/core/StashService"; export interface IPerformerCardExtraCriteria { scenes: Criterion[]; @@ -61,17 +62,39 @@ export const PerformerCard: React.FC = ({ { age, years_old: ageL10String } ); - function maybeRenderFavoriteIcon() { - if (performer.favorite === false) { - return; - } + const [updatePerformer] = usePerformerUpdate(); + + function renderFavoriteIcon() { return ( -
- -
+ e.preventDefault()}> + + ); } + function onToggleFavorite(v: boolean) { + if (performer.id) { + updatePerformer({ + variables: { + input: { + id: performer.id, + favorite: v, + }, + }, + }); + } + } + function maybeRenderScenesPopoverButton() { if (!performer.scene_count) return; @@ -214,7 +237,8 @@ export const PerformerCard: React.FC = ({ alt={performer.name ?? ""} src={performer.image_path ?? ""} /> - {maybeRenderFavoriteIcon()} + + {renderFavoriteIcon()} {maybeRenderRatingBanner()} {maybeRenderFlag()} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 8d7077406..357886ffb 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -12,17 +12,16 @@ import { usePerformerDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { - Counter, - CountryFlag, - DetailsEditNavbar, - ErrorMessage, - Icon, - LoadingIndicator, -} from "src/components/Shared"; -import { useLightbox, useToast } from "src/hooks"; +import { Counter } from "src/components/Shared/Counter"; +import { CountryFlag } from "src/components/Shared/CountryFlag"; +import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; +import { ErrorMessage } from "src/components/Shared/ErrorMessage"; +import { Icon } from "src/components/Shared/Icon"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { useLightbox } from "src/hooks/Lightbox/hooks"; +import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; import { PerformerScenesPanel } from "./PerformerScenesPanel"; @@ -32,12 +31,8 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerSubmitButton } from "./PerformerSubmitButton"; import GenderIcon from "../GenderIcon"; -import { - faCamera, - faDove, - faHeart, - faLink, -} from "@fortawesome/free-solid-svg-icons"; +import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons"; +import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { IUIConfig } from "src/core/config"; import { useRatingKeybinds } from "src/hooks/keybinds"; @@ -247,7 +242,6 @@ const PerformerPage: React.FC = ({ performer }) => { { @@ -351,7 +345,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} @@ -366,7 +360,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} @@ -405,7 +399,7 @@ const PerformerPage: React.FC = ({ performer }) => {

{performer.name} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx index 1427ceb4a..2e6bccded 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerCreate.tsx @@ -1,6 +1,6 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { PerformerEditPanel } from "./PerformerEditPanel"; import { useLocation } from "react-router-dom"; @@ -8,13 +8,11 @@ const PerformerCreate: React.FC = () => { const [imagePreview, setImagePreview] = useState(); const [imageEncoding, setImageEncoding] = useState(false); - function useQuery() { - const { search } = useLocation(); - return React.useMemo(() => new URLSearchParams(search), [search]); - } - - const query = useQuery(); - const nameQuery = query.get("name"); + const location = useLocation(); + const query = useMemo(() => new URLSearchParams(location.search), [location]); + const performer = { + name: query.get("q") ?? undefined, + }; const activeImage = imagePreview ?? ""; const intl = useIntl(); @@ -50,9 +48,8 @@ const PerformerCreate: React.FC = () => { />

diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index e8c4ffc3f..9a0aa9f07 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,8 +1,10 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { TagLink } from "src/components/Shared"; +import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils, getStashboxBase, getCountryByISO } from "src/utils"; +import TextUtils from "src/utils/text"; +import { getStashboxBase } from "src/utils/stashbox"; +import { getCountryByISO } from "src/utils/country"; import { TextField, URLField } from "src/utils/field"; import { cmToImperial, kgToLbs } from "src/utils/units"; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index f542ee7de..cc77ccda3 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -13,17 +13,17 @@ import { useTagCreate, queryScrapePerformerURL, } from "src/core/StashService"; -import { - Icon, - ImageInput, - LoadingIndicator, - CollapseButton, - TagSelect, - URLField, - CountrySelect, -} from "src/components/Shared"; -import { ImageUtils, getStashIDs } from "src/utils"; -import { useToast } from "src/hooks"; +import { Icon } from "src/components/Shared/Icon"; +import { ImageInput } from "src/components/Shared/ImageInput"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; +import { CollapseButton } from "src/components/Shared/CollapseButton"; +import { TagSelect } from "src/components/Shared/Select"; +import { CountrySelect } from "src/components/Shared/CountrySelect"; +import { URLField } from "src/components/Shared/URLField"; +import ImageUtils from "src/utils/image"; +import { getStashIDs } from "src/utils/stashIds"; +import { stashboxDisplayName } from "src/utils/stashbox"; +import { useToast } from "src/hooks/Toast"; import { Prompt, useHistory } from "react-router-dom"; import { useFormik } from "formik"; import { @@ -32,7 +32,6 @@ import { stringToGender, } from "src/utils/gender"; import { ConfigurationContext } from "src/hooks/Config"; -import { stashboxDisplayName } from "src/utils/stashbox"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; @@ -50,7 +49,6 @@ const isScraper = ( interface IPerformerDetails { performer: Partial; - isNew?: boolean; isVisible: boolean; onImageChange?: (image?: string | null) => void; onImageEncoding?: (loading?: boolean) => void; @@ -59,7 +57,6 @@ interface IPerformerDetails { export const PerformerEditPanel: React.FC = ({ performer, - isNew, isVisible, onImageChange, onImageEncoding, @@ -68,8 +65,10 @@ export const PerformerEditPanel: React.FC = ({ const Toast = useToast(); const history = useHistory(); - // Editing state - const [scraper, setScraper] = useState(); + const isNew = performer.id === undefined; + + // Editing stat + const [scraper, setScraper] = useState(); const [newTags, setNewTags] = useState(); const [isScraperModalOpen, setIsScraperModalOpen] = useState(false); @@ -128,7 +127,7 @@ export const PerformerEditPanel: React.FC = ({ twitter: yup.string().optional(), instagram: yup.string().optional(), tag_ids: yup.array(yup.string().required()).optional(), - stash_ids: yup.mixed().optional(), + stash_ids: yup.mixed().optional(), image: yup.string().optional().nullable(), details: yup.string().optional(), death_date: yup.string().optional(), @@ -447,10 +446,10 @@ export const PerformerEditPanel: React.FC = ({ return () => onImageChange?.(); }, [formik.values.image, onImageChange]); - useEffect(() => onImageEncoding?.(imageEncoding), [ - onImageEncoding, - imageEncoding, - ]); + useEffect( + () => onImageEncoding?.(imageEncoding), + [onImageEncoding, imageEncoding] + ); useEffect(() => { const newQueryableScrapers = ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index 96cfa40e7..0f62285f5 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -12,8 +12,8 @@ import { } from "src/components/Shared/ScrapeDialog"; import { useTagCreate } from "src/core/StashService"; import { Form } from "react-bootstrap"; -import { TagSelect } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import { TagSelect } from "src/components/Shared/Select"; +import { useToast } from "src/hooks/Toast"; import clone from "lodash-es/clone"; import { genderStrings, diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx index 7400e5608..b402bfcbe 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx @@ -4,7 +4,8 @@ import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { Modal, LoadingIndicator } from "src/components/Shared"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { useScrapePerformerList } from "src/core/StashService"; const CLASSNAME = "PerformerScrapeModal"; @@ -39,7 +40,7 @@ const PerformerScrapeModal: React.FC = ({ useEffect(() => inputRef.current?.focus(), []); return ( - = ({ )} - + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx index 39d87c788..9fc237905 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx @@ -4,7 +4,8 @@ import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { Modal, LoadingIndicator } from "src/components/Shared"; +import { ModalComponent } from "src/components/Shared/Modal"; +import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { stashboxDisplayName } from "src/utils/stashbox"; const CLASSNAME = "PerformerScrapeModal"; @@ -50,7 +51,7 @@ const PerformerStashBoxModal: React.FC = ({ useEffect(() => inputRef.current?.focus(), []); return ( - = ({ query !== "" &&
No results found.
)} -
+ ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index 0cd8902ae..1fa96304e 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -11,12 +11,16 @@ import { queryFindPerformers, usePerformersDestroy, } from "src/core/StashService"; -import { usePerformersList } from "src/hooks"; -import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; +import { + showWhenSelected, + PersistanceLevel, + usePerformersList, +} from "src/hooks/ListHook"; import { ListFilterModel } from "src/models/list-filter/filter"; import { DisplayMode } from "src/models/list-filter/types"; -import { PerformerTagger } from "src/components/Tagger"; -import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; +import { PerformerTagger } from "../Tagger/performers/PerformerTagger"; +import { ExportDialog } from "../Shared/ExportDialog"; +import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { IPerformerCardExtraCriteria, PerformerCard } from "./PerformerCard"; import { PerformerListTable } from "./PerformerListTable"; import { EditPerformersDialog } from "./EditPerformersDialog"; diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 0b7d1be57..1b2f858fd 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -5,8 +5,8 @@ import { useIntl } from "react-intl"; import { Button, Table } from "react-bootstrap"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { Icon } from "src/components/Shared"; -import { NavUtils } from "src/utils"; +import { Icon } from "../Shared/Icon"; +import NavUtils from "src/utils/navigation"; import { faHeart } from "@fortawesome/free-solid-svg-icons"; import { cmToImperial } from "src/utils/units"; diff --git a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx index 300b93f2f..40611967a 100644 --- a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx +++ b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx @@ -1,6 +1,6 @@ -import React, { FunctionComponent } from "react"; +import React from "react"; import { useFindPerformers } from "src/core/StashService"; -import Slider from "react-slick"; +import Slider from "@ant-design/react-slick"; import { PerformerCard } from "./PerformerCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; @@ -13,9 +13,7 @@ interface IProps { header: string; } -export const PerformerRecommendationRow: FunctionComponent = ( - props: IProps -) => { +export const PerformerRecommendationRow: React.FC = (props) => { const result = useFindPerformers(props.filter); const cardCount = result.data?.findPerformers.count; diff --git a/ui/v2.5/src/components/Performers/Performers.tsx b/ui/v2.5/src/components/Performers/Performers.tsx index 027b441bd..f919fdba5 100644 --- a/ui/v2.5/src/components/Performers/Performers.tsx +++ b/ui/v2.5/src/components/Performers/Performers.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; -import { TITLE_SUFFIX } from "src/components/Shared"; +import { TITLE_SUFFIX } from "src/components/Shared/constants"; import { PersistanceLevel } from "src/hooks/ListHook"; import Performer from "./PerformerDetails/Performer"; import PerformerCreate from "./PerformerDetails/PerformerCreate"; diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss index 1e8fbb7ba..2ec6433d4 100644 --- a/ui/v2.5/src/components/Performers/styles.scss +++ b/ui/v2.5/src/components/Performers/styles.scss @@ -76,7 +76,7 @@ width: 100%; } - .flag-icon { + .fi { bottom: 1rem; filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); height: 2rem; @@ -85,12 +85,41 @@ width: 3rem; } - .favorite { - color: #ff7373; - filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); + button.btn.favorite-button { + opacity: 1; + padding: 0; position: absolute; right: 5px; top: 10px; + transition: opacity 0.5s; + + svg.fa-icon { + margin-left: 0.4rem; + margin-right: 0.4rem; + } + + &.not-favorite { + color: rgba(191, 204, 214, 0.5); + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); + opacity: 0; + } + + &.favorite { + color: #ff7373; + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); + } + + &:hover, + &:active, + &:focus, + &:active:focus { + background: none; + box-shadow: none; + } + } + + &:hover button.btn.favorite-button.not-favorite { + opacity: 1; } &__age { diff --git a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx index 521ec41c6..882664d26 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -12,19 +12,16 @@ import { } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; -import querystring from "query-string"; import * as GQL from "src/core/generated-graphql"; -import { - LoadingIndicator, - ErrorMessage, - HoverPopover, - Icon, - TagLink, - SweatDrops, -} from "src/components/Shared"; +import { LoadingIndicator } from "../Shared/LoadingIndicator"; +import { ErrorMessage } from "../Shared/ErrorMessage"; +import { HoverPopover } from "../Shared/HoverPopover"; +import { Icon } from "../Shared/Icon"; +import { TagLink } from "../Shared/TagLink"; +import { SweatDrops } from "../Shared/SweatDrops"; import { Pagination } from "src/components/List/Pagination"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import { DeleteScenesDialog } from "src/components/Scenes/DeleteScenesDialog"; import { EditScenesDialog } from "../Scenes/EditScenesDialog"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; @@ -47,20 +44,13 @@ const CLASSNAME = "duplicate-checker"; export const SceneDuplicateChecker: React.FC = () => { const intl = useIntl(); const history = useHistory(); - const { page, size, distance } = querystring.parse(history.location.search); - const currentPage = Number.parseInt( - Array.isArray(page) ? page[0] : page ?? "1", - 10 - ); - const pageSize = Number.parseInt( - Array.isArray(size) ? size[0] : size ?? "20", - 10 - ); + + const query = new URLSearchParams(history.location.search); + const currentPage = Number.parseInt(query.get("page") ?? "1", 10); + const pageSize = Number.parseInt(query.get("size") ?? "20", 10); + const hashDistance = Number.parseInt(query.get("distance") ?? "0", 10); + const [currentPageSize, setCurrentPageSize] = useState(pageSize); - const hashDistance = Number.parseInt( - Array.isArray(distance) ? distance[0] : distance ?? "0", - 10 - ); const [isMultiDelete, setIsMultiDelete] = useState(false); const [deletingScenes, setDeletingScenes] = useState(false); const [editingScenes, setEditingScenes] = useState(false); @@ -90,9 +80,8 @@ export const SceneDuplicateChecker: React.FC = () => { GQL.SlimSceneDataFragment[] | null >(null); - const [mergeScenes, setMergeScenes] = useState< - { id: string; title: string }[] | undefined - >(undefined); + const [mergeScenes, setMergeScenes] = + useState<{ id: string; title: string }[]>(); if (loading) return ; if (!data) return ; @@ -107,12 +96,16 @@ export const SceneDuplicateChecker: React.FC = () => { ).length; const setQuery = (q: Record) => { - history.push({ - search: querystring.stringify({ - ...querystring.parse(history.location.search), - ...q, - }), - }); + const newQuery = new URLSearchParams(query); + for (const key of Object.keys(q)) { + const value = q[key]; + if (value !== undefined) { + newQuery.set(key, String(value)); + } else { + newQuery.delete(key); + } + } + history.push({ search: newQuery.toString() }); }; function onDeleteDialogClosed(deleted: boolean) { @@ -504,7 +497,7 @@ export const SceneDuplicateChecker: React.FC = () => { page: undefined, }) } - defaultValue={distance ?? 0} + defaultValue={hashDistance} className="input-control ml-4" >