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
[](https://github.com/stashapp/stash/actions/workflows/build.yml)
[](https://hub.docker.com/r/stashapp/stash 'DockerHub')
+[](https://opencollective.com/stashapp)
[](https://goreportcard.com/report/github.com/stashapp/stash)
[](https://discord.gg/2TsNFKt)
+[](https://github.com/stashapp/stash/releases/latest)
+[](https://github.com/stashapp/stash/labels/bounty)
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**

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"
>