mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Merge branch 'develop' into releases/0.19.1
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
# GraphQL generated output
|
# GraphQL generated output
|
||||||
internal/api/generated_*.go
|
internal/api/generated_*.go
|
||||||
ui/v2.5/src/core/generated-*.tsx
|
|
||||||
|
|
||||||
####
|
####
|
||||||
# Jetbrains
|
# Jetbrains
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ https://stashapp.cc
|
|||||||
|
|
||||||
[](https://github.com/stashapp/stash/actions/workflows/build.yml)
|
[](https://github.com/stashapp/stash/actions/workflows/build.yml)
|
||||||
[](https://hub.docker.com/r/stashapp/stash 'DockerHub')
|
[](https://hub.docker.com/r/stashapp/stash 'DockerHub')
|
||||||
|
[](https://opencollective.com/stashapp)
|
||||||
[](https://goreportcard.com/report/github.com/stashapp/stash)
|
[](https://goreportcard.com/report/github.com/stashapp/stash)
|
||||||
[](https://discord.gg/2TsNFKt)
|
[](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.**
|
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
|
||||||

|

|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -58,6 +58,7 @@ require (
|
|||||||
github.com/vearutop/statigz v1.1.6
|
github.com/vearutop/statigz v1.1.6
|
||||||
github.com/vektah/dataloaden v0.3.0
|
github.com/vektah/dataloaden v0.3.0
|
||||||
github.com/vektah/gqlparser/v2 v2.4.1
|
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
|
gopkg.in/guregu/null.v4 v4.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
2
go.sum
2
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 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
|
||||||
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
|
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/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/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/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Gallery.Find(ctx, idInt)
|
ret, err = r.repository.Gallery.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"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)
|
image, err = qb.Find(ctx, idInt)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if checksum != nil {
|
} else if checksum != nil {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Movie.Find(ctx, idInt)
|
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +36,6 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
|
|||||||
Count: total,
|
Count: total,
|
||||||
Movies: movies,
|
Movies: movies,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Performer.Find(ctx, idInt)
|
ret, err = r.repository.Performer.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
|
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ret, 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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
|
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ret, err
|
return ret, err
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
@@ -21,7 +23,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scene, err = qb.Find(ctx, idInt)
|
scene, err = qb.Find(ctx, idInt)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if checksum != nil {
|
} else if checksum != nil {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
@@ -17,7 +19,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
|||||||
var err error
|
var err error
|
||||||
ret, err = r.repository.Studio.Find(ctx, idInt)
|
ret, err = r.repository.Studio.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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 {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
ret, err = r.repository.Tag.Find(ctx, idInt)
|
ret, err = r.repository.Tag.Find(ctx, idInt)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
bufferReader := bytes.NewReader(buffer.Bytes())
|
||||||
http.ServeContent(w, r, name, latestModTime, bufferReader)
|
http.ServeContent(w, r, name, latestModTime, bufferReader)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
|||||||
return
|
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)
|
logger.Errorf("error generating preview: %v", err)
|
||||||
logErrorOutput(err)
|
logErrorOutput(err)
|
||||||
return
|
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
|
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")
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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
|
// scan zip files with a different context that is not cancellable
|
||||||
// cancelling while scanning zip file contents results in the scan
|
// cancelling while scanning zip file contents results in the scan
|
||||||
// contents being partially completed
|
// contents being partially completed
|
||||||
zipCtx := context.Background()
|
zipCtx := utils.ValueOnlyContext{Context: ctx}
|
||||||
|
|
||||||
if err := s.scanZipFile(zipCtx, f); err != nil {
|
if err := s.scanZipFile(zipCtx, f); err != nil {
|
||||||
logger.Errorf("Error scanning zip file %q: %v", f.Path, err)
|
logger.Errorf("Error scanning zip file %q: %v", f.Path, err)
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package file
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/xWTF/chardet"
|
||||||
|
|
||||||
|
"golang.org/x/net/html/charset"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -40,6 +47,42 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) {
|
|||||||
return nil, err
|
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{
|
return &ZipFS{
|
||||||
Reader: zipReader,
|
Reader: zipReader,
|
||||||
zipFileCloser: reader,
|
zipFileCloser: reader,
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxGraveyardSize = 10
|
const maxGraveyardSize = 10
|
||||||
@@ -178,7 +179,7 @@ func (m *Manager) dispatch(ctx context.Context, j *Job) (done chan struct{}) {
|
|||||||
j.StartTime = &t
|
j.StartTime = &t
|
||||||
j.Status = StatusRunning
|
j.Status = StatusRunning
|
||||||
|
|
||||||
ctx, cancelFunc := context.WithCancel(valueOnlyContext{ctx})
|
ctx, cancelFunc := context.WithCancel(utils.ValueOnlyContext{Context: ctx})
|
||||||
j.cancelFunc = cancelFunc
|
j.cancelFunc = cancelFunc
|
||||||
|
|
||||||
done = make(chan struct{})
|
done = make(chan struct{})
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ func (g PreviewOptions) getStepSizeAndOffset(videoDuration float64) (stepSize fl
|
|||||||
return
|
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)
|
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||||
defer lockCtx.Cancel()
|
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)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +90,10 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration
|
|||||||
return nil
|
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
|
// #2496 - generate a single preview video for videos shorter than segments * segment duration
|
||||||
if videoDuration < options.SegmentDuration*float64(options.Segments) {
|
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 {
|
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,
|
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
|
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 {
|
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||||
chunkOptions := previewChunkOptions{
|
chunkOptions := previewChunkOptions{
|
||||||
StartTime: 0,
|
StartTime: 0,
|
||||||
@@ -160,7 +160,7 @@ func (g *Generator) previewVideoSingle(input string, videoDuration float64, opti
|
|||||||
Preset: options.Preset,
|
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
|
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
|
var videoFilter ffmpeg.VideoFilter
|
||||||
videoFilter = videoFilter.ScaleWidth(scenePreviewWidth)
|
videoFilter = videoFilter.ScaleWidth(scenePreviewWidth)
|
||||||
|
|
||||||
@@ -189,6 +189,10 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt
|
|||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if useVsync2 {
|
||||||
|
videoArgs = append(videoArgs, "-vsync", "2")
|
||||||
|
}
|
||||||
|
|
||||||
trimOptions := transcoder.TranscodeOptions{
|
trimOptions := transcoder.TranscodeOptions{
|
||||||
OutputPath: options.OutputPath,
|
OutputPath: options.OutputPath,
|
||||||
StartTime: options.StartTime,
|
StartTime: options.StartTime,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123
|
|||||||
|
|
||||||
func randomSequence(n int) string {
|
func randomSequence(n int) string {
|
||||||
b := make([]rune, n)
|
b := make([]rune, n)
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
for i := range b {
|
for i := range b {
|
||||||
b[i] = characters[rand.Intn(len(characters))]
|
b[i] = characters[rand.Intn(len(characters))]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -707,6 +707,13 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen
|
|||||||
ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images)
|
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 {
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
||||||
pqb := c.repository.Performer
|
pqb := c.repository.Performer
|
||||||
tqb := c.repository.Tag
|
tqb := c.repository.Tag
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -1706,5 +1707,26 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int) ([][]*mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortByPath(duplicates)
|
||||||
|
|
||||||
return duplicates, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
22
pkg/utils/context.go
Normal file
22
pkg/utils/context.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
BROWSER=none
|
BROWSER=none
|
||||||
PORT=3000
|
|
||||||
ESLINT_NO_DEV_ERRORS=true
|
ESLINT_NO_DEV_ERRORS=true
|
||||||
|
|||||||
@@ -9,23 +9,21 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["@typescript-eslint", "jsx-a11y"],
|
||||||
"@typescript-eslint",
|
|
||||||
"jsx-a11y"
|
|
||||||
],
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb-typescript",
|
"airbnb-typescript",
|
||||||
"airbnb/hooks",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:import/recommended",
|
"plugin:import/recommended",
|
||||||
"prettier",
|
"plugin:react/recommended",
|
||||||
"prettier/prettier"
|
"plugin:react/jsx-runtime",
|
||||||
|
"airbnb/hooks",
|
||||||
|
"prettier"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
"version": "detect"
|
"version": "detect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-explicit-any": 2,
|
"@typescript-eslint/no-explicit-any": 2,
|
||||||
"@typescript-eslint/naming-convention": [
|
"@typescript-eslint/naming-convention": [
|
||||||
@@ -56,14 +54,24 @@
|
|||||||
"import/no-unresolved": "off",
|
"import/no-unresolved": "off",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react/style-prop-object": ["error", {
|
"react/style-prop-object": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
"allow": ["FormattedNumber"]
|
"allow": ["FormattedNumber"]
|
||||||
}],
|
}
|
||||||
"spaced-comment": ["error", "always", {
|
],
|
||||||
|
"spaced-comment": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
"markers": ["/"]
|
"markers": ["/"]
|
||||||
}],
|
}
|
||||||
|
],
|
||||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }],
|
"@typescript-eslint/no-use-before-define": [
|
||||||
|
"error",
|
||||||
|
{ "functions": false, "classes": true }
|
||||||
|
],
|
||||||
"no-nested-ternary": "off"
|
"no-nested-ternary": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
ui/v2.5/.gitignore
vendored
5
ui/v2.5/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# generated
|
||||||
|
src/core/generated-*.tsx
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
/build
|
/build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
|
.gitignore
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
@@ -23,3 +25,4 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
|||||||
18
ui/v2.5/.prettierignore
Normal file
18
ui/v2.5/.prettierignore
Normal file
@@ -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
|
||||||
@@ -1,91 +1,55 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": ["stylelint-order"],
|
||||||
"stylelint-order"
|
"customSyntax": "postcss-scss",
|
||||||
],
|
|
||||||
"extends": "stylelint-config-prettier",
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indentation": null,
|
"at-rule-empty-line-before": [
|
||||||
"at-rule-empty-line-before": [ "always", {
|
"always",
|
||||||
except: ["after-same-name", "first-nested" ],
|
{
|
||||||
ignore: ["after-comment"],
|
"except": ["after-same-name", "first-nested"],
|
||||||
} ],
|
"ignore": ["after-comment"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"at-rule-no-vendor-prefix": true,
|
"at-rule-no-vendor-prefix": true,
|
||||||
"selector-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-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-hex-length": "short",
|
||||||
"color-no-invalid-hex": true,
|
"color-no-invalid-hex": true,
|
||||||
"comment-empty-line-before": [ "always", {
|
"comment-empty-line-before": [
|
||||||
except: ["first-nested"],
|
"always",
|
||||||
ignore: ["stylelint-commands"],
|
{
|
||||||
} ],
|
"except": ["first-nested"],
|
||||||
|
"ignore": ["stylelint-commands"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"comment-whitespace-inside": "always",
|
"comment-whitespace-inside": "always",
|
||||||
"declaration-bang-space-after": "never",
|
|
||||||
"declaration-bang-space-before": "always",
|
|
||||||
"declaration-block-no-shorthand-property-overrides": true,
|
"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-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,
|
"declaration-no-important": true,
|
||||||
"font-family-name-quotes": "always-where-recommended",
|
"font-family-name-quotes": "always-where-recommended",
|
||||||
"function-calc-no-unspaced-operator": true,
|
"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-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-url-quotes": "always",
|
||||||
"function-whitespace-after": "always",
|
|
||||||
"length-zero-no-unit": true,
|
"length-zero-no-unit": true,
|
||||||
"max-empty-lines": 1,
|
|
||||||
"max-nesting-depth": 4,
|
"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-descending-specificity": null,
|
||||||
"no-invalid-double-slash-comments": true,
|
"no-invalid-double-slash-comments": true,
|
||||||
"no-missing-end-of-source-newline": true,
|
|
||||||
"number-max-precision": 3,
|
"number-max-precision": 3,
|
||||||
"number-no-trailing-zeros": true,
|
"order/order": ["custom-properties", "declarations"],
|
||||||
"order/order": [
|
|
||||||
"custom-properties",
|
|
||||||
"declarations"
|
|
||||||
],
|
|
||||||
"order/properties-alphabetical-order": true,
|
"order/properties-alphabetical-order": true,
|
||||||
"rule-empty-line-before": ["always-multi-line", {
|
"rule-empty-line-before": [
|
||||||
except: ["after-single-line-comment", "first-nested" ],
|
"always-multi-line",
|
||||||
ignore: ["after-comment"],
|
{
|
||||||
}],
|
"except": ["after-single-line-comment", "first-nested"],
|
||||||
|
"ignore": ["after-comment"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"selector-max-id": 1,
|
"selector-max-id": 1,
|
||||||
"selector-max-type": 2,
|
"selector-max-type": 2,
|
||||||
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
|
"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-max-universal": 0,
|
||||||
"selector-type-case": "lower",
|
"selector-type-case": "lower",
|
||||||
"selector-pseudo-element-colon-notation": "double",
|
"selector-pseudo-element-colon-notation": "double",
|
||||||
"string-no-newline": true,
|
"string-no-newline": true,
|
||||||
"string-quotes": "double",
|
"time-min-milliseconds": 100
|
||||||
"time-min-milliseconds": 100,
|
}
|
||||||
"value-list-comma-space-after": "always-single-line",
|
|
||||||
"value-list-comma-space-before": "never"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
18
ui/v2.5/.vscode/launch.json
vendored
18
ui/v2.5/.vscode/launch.json
vendored
@@ -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}/*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
26
ui/v2.5/.vscode/settings.json
vendored
26
ui/v2.5/.vscode/settings.json
vendored
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,6 @@ documents: "../../graphql/documents/**/*.graphql"
|
|||||||
generates:
|
generates:
|
||||||
src/core/generated-graphql.tsx:
|
src/core/generated-graphql.tsx:
|
||||||
plugins:
|
plugins:
|
||||||
- add:
|
|
||||||
content: "/* eslint-disable */"
|
|
||||||
- time
|
- time
|
||||||
- typescript
|
- typescript
|
||||||
- typescript-operations
|
- typescript-operations
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<base href="/">
|
<base href="/" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="shortcut icon" href="favicon.ico" />
|
<link rel="shortcut icon" href="favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
@@ -10,27 +10,15 @@
|
|||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="%COLOR%" />
|
<meta name="theme-color" content="%COLOR%" />
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
|
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
|
||||||
<title>Stash</title>
|
<title>Stash</title>
|
||||||
<script>window.STASH_BASE_URL = "/%BASE_URL%/"</script>
|
<script>
|
||||||
|
window.STASH_BASE_URL = "/%BASE_URL%/";
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,125 +3,116 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"sideEffects": false,
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build-ci": "yarn validate && yarn build",
|
"build-ci": "yarn run validate && yarn run build",
|
||||||
"validate": "yarn lint && tsc --noEmit && yarn format-check",
|
"validate": "yarn run lint && yarn run check && yarn run format-check",
|
||||||
"lint": "yarn lint:css && yarn lint:js",
|
"lint": "yarn run lint:css && yarn run lint:js",
|
||||||
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
"lint:css": "stylelint --cache \"src/**/*.scss\"",
|
||||||
"lint:css": "stylelint \"src/**/*.scss\"",
|
"lint:js": "eslint --cache src/",
|
||||||
"format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
"check": "tsc --noEmit",
|
||||||
"format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
"format": "prettier --write .",
|
||||||
|
"format-check": "prettier --check .",
|
||||||
"gqlgen": "gql-gen --config codegen.yml",
|
"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'"
|
"extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
">0.2%",
|
">0.5% and supports es6-module-dynamic-import"
|
||||||
"not dead",
|
|
||||||
"not ie <= 11",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.7",
|
"@ant-design/react-slick": "^1.0.0",
|
||||||
"@formatjs/intl-getcanonicallocales": "^1.5.3",
|
"@apollo/client": "^3.7.8",
|
||||||
"@formatjs/intl-locale": "^2.4.14",
|
"@formatjs/intl-getcanonicallocales": "^2.0.5",
|
||||||
"@formatjs/intl-numberformat": "^6.1.3",
|
"@formatjs/intl-locale": "^3.0.11",
|
||||||
"@formatjs/intl-pluralrules": "^4.0.6",
|
"@formatjs/intl-numberformat": "^8.3.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
"@formatjs/intl-pluralrules": "^5.1.8",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||||
"@types/react-select": "^4.0.8",
|
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||||
"ansi-regex": "^5.0.1",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"apollo-upload-client": "^14.1.3",
|
"apollo-upload-client": "^17.0.0",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.3.3",
|
||||||
"base64-blob": "^1.4.1",
|
"base64-blob": "^1.4.1",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.2",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.3.2",
|
||||||
"flag-icon-css": "^3.5.0",
|
"flag-icons": "^6.6.6",
|
||||||
"flexbin": "^0.2.0",
|
"flexbin": "^0.2.0",
|
||||||
"formik": "^2.2.6",
|
"formik": "^2.2.9",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-tag": "^2.11.0",
|
"graphql-tag": "^2.12.6",
|
||||||
"i18n-iso-countries": "^6.4.0",
|
"graphql-ws": "^5.11.3",
|
||||||
"intersection-observer": "^0.12.0",
|
"i18n-iso-countries": "^7.5.0",
|
||||||
"localforage": "^1.9.0",
|
"intersection-observer": "^0.12.2",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"mousetrap-pause": "^1.0.0",
|
"mousetrap-pause": "^1.0.0",
|
||||||
"normalize-url": "^4.5.1",
|
"normalize-url": "^4.5.1",
|
||||||
"postcss": "^8.2.10",
|
"react": "^17.0.2",
|
||||||
"query-string": "6.13.8",
|
"react-bootstrap": "^1.6.6",
|
||||||
"react": "17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-bootstrap": "1.4.3",
|
|
||||||
"react-dom": "17.0.2",
|
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "^5.10.16",
|
"react-intl": "^6.2.8",
|
||||||
"react-markdown": "^7.1.0",
|
"react-markdown": "^8.0.5",
|
||||||
"react-router-bootstrap": "^0.25.0",
|
"react-router-bootstrap": "^0.25.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-router-hash-link": "^2.3.1",
|
"react-router-hash-link": "^2.4.3",
|
||||||
"react-select": "^4.0.2",
|
"react-select": "^5.7.0",
|
||||||
"react-slick": "^0.29.0",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-gfm": "^1.0.0",
|
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.32.5",
|
|
||||||
"slick-carousel": "^1.8.1",
|
"slick-carousel": "^1.8.1",
|
||||||
"string.prototype.replaceall": "^1.0.4",
|
"string.prototype.replaceall": "^1.0.7",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
|
||||||
"thehandy": "^1.0.3",
|
"thehandy": "^1.0.3",
|
||||||
"universal-cookie": "^4.0.4",
|
"universal-cookie": "^4.0.4",
|
||||||
"video.js": "^7.20.3",
|
"video.js": "^7.21.1",
|
||||||
"videojs-mobile-ui": "^0.8.0",
|
"videojs-mobile-ui": "^0.8.0",
|
||||||
"videojs-seek-buttons": "^3.0.1",
|
"videojs-seek-buttons": "^3.0.1",
|
||||||
"videojs-vtt.js": "^0.15.4",
|
"videojs-vtt.js": "^0.15.4",
|
||||||
"vite": "^2.9.13",
|
"yup": "^1.0.0"
|
||||||
"vite-plugin-compression": "^0.3.5",
|
|
||||||
"vite-tsconfig-paths": "^3.3.17",
|
|
||||||
"ws": "^7.4.6",
|
|
||||||
"yup": "^0.32.9"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/add": "^2.0.2",
|
"@babel/core": "^7.20.12",
|
||||||
"@graphql-codegen/cli": "^1.20.0",
|
"@graphql-codegen/cli": "^3.0.0",
|
||||||
"@graphql-codegen/time": "^2.0.2",
|
"@graphql-codegen/time": "^4.0.0",
|
||||||
"@graphql-codegen/typescript": "^1.20.00",
|
"@graphql-codegen/typescript": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-operations": "^1.17.13",
|
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^2.2.1",
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||||
"@types/apollo-upload-client": "^14.1.0",
|
"@types/apollo-upload-client": "^17.0.2",
|
||||||
"@types/classnames": "^2.2.11",
|
|
||||||
"@types/fslightbox-react": "^1.4.0",
|
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
"@types/mousetrap": "^1.6.5",
|
"@types/mousetrap": "^1.6.11",
|
||||||
"@types/node": "14.14.22",
|
"@types/node": "^18.13.0",
|
||||||
"@types/react": "17.0.31",
|
"@types/react": "^17.0.53",
|
||||||
"@types/react-dom": "^17.0.10",
|
"@types/react-dom": "^17.0.19",
|
||||||
"@types/react-helmet": "^6.1.3",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@types/react-router-bootstrap": "^0.24.5",
|
"@types/react-router-bootstrap": "^0.24.5",
|
||||||
"@types/react-router-dom": "5.1.7",
|
"@types/react-router-hash-link": "^2.4.5",
|
||||||
"@types/react-router-hash-link": "^1.2.1",
|
"@types/video.js": "^7.3.51",
|
||||||
"@types/react-slick": "^0.23.8",
|
|
||||||
"@types/video.js": "^7.3.49",
|
|
||||||
"@types/videojs-mobile-ui": "^0.5.0",
|
"@types/videojs-mobile-ui": "^0.5.0",
|
||||||
"@types/videojs-seek-buttons": "^2.1.0",
|
"@types/videojs-seek-buttons": "^2.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"eslint": "^7.32.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
"eslint": "^8.34.0",
|
||||||
"eslint-config-airbnb-typescript": "^14.0.1",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-react": "^7.26.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"postcss-safe-parser": "^5.0.2",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "2.2.1",
|
"postcss-scss": "^4.0.6",
|
||||||
"stylelint": "^13.9.0",
|
"prettier": "^2.8.4",
|
||||||
"stylelint-config-prettier": "^8.0.2",
|
"sass": "^1.58.1",
|
||||||
"stylelint-order": "^4.1.0",
|
"stylelint": "^15.1.0",
|
||||||
"typescript": "~4.4.4"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
12
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
12
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
declare module "videojs-vtt.js" {
|
declare module "videojs-vtt.js" {
|
||||||
namespace vttjs {
|
|
||||||
/**
|
/**
|
||||||
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
|
* 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.
|
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
|
||||||
@@ -18,7 +17,7 @@ declare module "videojs-vtt.js" {
|
|||||||
readonly message: string;
|
readonly message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace WebVTT {
|
export namespace WebVTT {
|
||||||
/**
|
/**
|
||||||
* A parser for the WebVTT spec in JavaScript.
|
* A parser for the WebVTT spec in JavaScript.
|
||||||
*/
|
*/
|
||||||
@@ -35,7 +34,11 @@ declare module "videojs-vtt.js" {
|
|||||||
*/
|
*/
|
||||||
constructor(window: Window);
|
constructor(window: Window);
|
||||||
constructor(window: Window, decoder: TextDecoder);
|
constructor(window: Window, decoder: TextDecoder);
|
||||||
constructor(window: Window, vttjs: vttjs, 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.
|
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
|
||||||
@@ -106,6 +109,3 @@ declare module "videojs-vtt.js" {
|
|||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export = vttjs;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Helmet } from "react-helmet";
|
|||||||
import cloneDeep from "lodash-es/cloneDeep";
|
import cloneDeep from "lodash-es/cloneDeep";
|
||||||
import mergeWith from "lodash-es/mergeWith";
|
import mergeWith from "lodash-es/mergeWith";
|
||||||
import { ToastProvider } from "src/hooks/Toast";
|
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 { initPolyfills } from "src/polyfills";
|
||||||
|
|
||||||
import locales, { registerCountry } from "src/locales";
|
import locales, { registerCountry } from "src/locales";
|
||||||
@@ -14,14 +14,15 @@ import {
|
|||||||
useConfigureUI,
|
useConfigureUI,
|
||||||
useSystemStatus,
|
useSystemStatus,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { flattenMessages } from "src/utils";
|
import flattenMessages from "./utils/flattenMessages";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import MousetrapPause from "mousetrap-pause";
|
import MousetrapPause from "mousetrap-pause";
|
||||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
import { MainNavbar } from "./components/MainNavbar";
|
import { MainNavbar } from "./components/MainNavbar";
|
||||||
import { PageNotFound } from "./components/PageNotFound";
|
import { PageNotFound } from "./components/PageNotFound";
|
||||||
import * as GQL from "./core/generated-graphql";
|
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 { ConfigurationProvider } from "./hooks/Config";
|
||||||
import { ManualProvider } from "./components/Help/context";
|
import { ManualProvider } from "./components/Help/context";
|
||||||
@@ -91,10 +92,12 @@ export const App: React.FC = () => {
|
|||||||
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
||||||
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
||||||
const chosenMessages = (await locales[messageLanguage]()).default;
|
const chosenMessages = (await locales[messageLanguage]()).default;
|
||||||
const res = await fetch(getPlatformURL() + "customlocales");
|
|
||||||
let customMessages = {};
|
let customMessages = {};
|
||||||
try {
|
try {
|
||||||
customMessages = res.ok ? await res.json() : {};
|
const res = await fetch(getPlatformURL() + "customlocales");
|
||||||
|
if (res.ok) {
|
||||||
|
customMessages = await res.json();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useChangelogStorage } from "src/hooks";
|
import { useChangelogStorage } from "src/hooks/LocalForage";
|
||||||
import Version from "./Version";
|
import Version from "./Version";
|
||||||
import V010 from "src/docs/en/Changelog/v010.md";
|
import V010 from "src/docs/en/Changelog/v010.md";
|
||||||
import V011 from "src/docs/en/Changelog/v011.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 V0190 from "src/docs/en/Changelog/v0190.md";
|
||||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||||
|
|
||||||
// to avoid use of explicit any
|
|
||||||
type Module = typeof V010;
|
|
||||||
|
|
||||||
const Changelog: React.FC = () => {
|
const Changelog: React.FC = () => {
|
||||||
const [{ data, loading }, setOpenState] = useChangelogStorage();
|
const [{ data, loading }, setOpenState] = useChangelogStorage();
|
||||||
|
|
||||||
@@ -55,7 +52,7 @@ const Changelog: React.FC = () => {
|
|||||||
interface IStashRelease {
|
interface IStashRelease {
|
||||||
version: string;
|
version: string;
|
||||||
date?: string;
|
date?: string;
|
||||||
page: Module;
|
page: string;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Card, Collapse } from "react-bootstrap";
|
import { Button, Card, Collapse } from "react-bootstrap";
|
||||||
import { FormattedDate, FormattedMessage } from "react-intl";
|
import { FormattedDate, FormattedMessage } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
|
||||||
interface IVersionProps {
|
interface IVersionProps {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Form, Button } from "react-bootstrap";
|
import { Form, Button } from "react-bootstrap";
|
||||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||||
import { Modal, Icon } from "src/components/Shared";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { useToast } from "src/hooks";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { useToast } from "src/hooks/Toast";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { Manual } from "../Help/Manual";
|
import { Manual } from "../Help/Manual";
|
||||||
import { withoutTypename } from "src/utils";
|
import { withoutTypename } from "src/utils/data";
|
||||||
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
||||||
import { SettingSection } from "../Settings/SettingSection";
|
import { SettingSection } from "../Settings/SettingSection";
|
||||||
import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -169,7 +170,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
modalProps={{ animation, size: "lg" }}
|
modalProps={{ animation, size: "lg" }}
|
||||||
icon={faCogs}
|
icon={faCogs}
|
||||||
@@ -203,7 +204,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
/>
|
/>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { Form, Button, Table } from "react-bootstrap";
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
@@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC<IFieldOptionsList> = ({
|
|||||||
allowSetDefault = true,
|
allowSetDefault = true,
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
}) => {
|
}) => {
|
||||||
const [localFieldOptions, setLocalFieldOptions] = useState<
|
const [localFieldOptions, setLocalFieldOptions] =
|
||||||
GQL.IdentifyFieldOptions[]
|
useState<GQL.IdentifyFieldOptions[]>();
|
||||||
>();
|
|
||||||
const [editField, setEditField] = useState<string | undefined>();
|
const [editField, setEditField] = useState<string | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
useConfigureDefaults,
|
useConfigureDefaults,
|
||||||
useListSceneScrapers,
|
useListSceneScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Icon, Modal, OperationButton } from "src/components/Shared";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { useToast } from "src/hooks";
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { withoutTypename } from "src/utils";
|
import { withoutTypename } from "src/utils/data";
|
||||||
import {
|
import {
|
||||||
SCRAPER_PREFIX,
|
SCRAPER_PREFIX,
|
||||||
STASH_BOX_PREFIX,
|
STASH_BOX_PREFIX,
|
||||||
@@ -202,9 +204,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
|
|
||||||
if (s.options) {
|
if (s.options) {
|
||||||
const sourceOptions = withoutTypename(s.options);
|
const sourceOptions = withoutTypename(s.options);
|
||||||
sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map(
|
sourceOptions.fieldOptions =
|
||||||
withoutTypename
|
sourceOptions.fieldOptions?.map(withoutTypename);
|
||||||
);
|
|
||||||
ret.options = sourceOptions;
|
ret.options = sourceOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +216,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
setSources(mappedSources);
|
setSources(mappedSources);
|
||||||
if (identifyDefaults.options) {
|
if (identifyDefaults.options) {
|
||||||
const defaultOptions = withoutTypename(identifyDefaults.options);
|
const defaultOptions = withoutTypename(identifyDefaults.options);
|
||||||
defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map(
|
defaultOptions.fieldOptions =
|
||||||
withoutTypename
|
defaultOptions.fieldOptions?.map(withoutTypename);
|
||||||
);
|
|
||||||
setOptions(defaultOptions);
|
setOptions(defaultOptions);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -405,7 +405,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
modalProps={{ animation, size: "lg" }}
|
modalProps={{ animation, size: "lg" }}
|
||||||
show
|
show
|
||||||
icon={faCogs}
|
icon={faCogs}
|
||||||
@@ -453,7 +453,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
setEditingField={(v) => setEditingField(v)}
|
setEditingField={(v) => setEditingField(v)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Form, Button, ListGroup } from "react-bootstrap";
|
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 { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { IScraperSource } from "./constants";
|
import { IScraperSource } from "./constants";
|
||||||
@@ -53,7 +54,7 @@ export const SourcesEditor: React.FC<ISourceEditor> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
dialogClassName="identify-source-editor"
|
dialogClassName="identify-source-editor"
|
||||||
modalProps={{ animation: false, size: "lg" }}
|
modalProps={{ animation: false, size: "lg" }}
|
||||||
show
|
show
|
||||||
@@ -107,7 +108,7 @@ export const SourcesEditor: React.FC<ISourceEditor> = ({
|
|||||||
defaultOptions={defaultOptions}
|
defaultOptions={defaultOptions}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const sceneFields = [
|
|||||||
"tags",
|
"tags",
|
||||||
"stash_ids",
|
"stash_ids",
|
||||||
] as const;
|
] as const;
|
||||||
export type SceneField = typeof sceneFields[number];
|
export type SceneField = (typeof sceneFields)[number];
|
||||||
|
|
||||||
export const multiValueSceneFields: SceneField[] = [
|
export const multiValueSceneFields: SceneField[] = [
|
||||||
"studio",
|
"studio",
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
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 { faCogs } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||||
import { Module } from "src/docs/en/ReleaseNotes";
|
|
||||||
|
|
||||||
interface IReleaseNotesDialog {
|
interface IReleaseNotesDialog {
|
||||||
notes: Module[];
|
notes: string[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ export const ReleaseNotesDialog: React.FC<IReleaseNotesDialog> = ({
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faCogs}
|
icon={faCogs}
|
||||||
header={intl.formatMessage({ id: "release_notes" })}
|
header={intl.formatMessage({ id: "release_notes" })}
|
||||||
@@ -32,7 +31,7 @@ export const ReleaseNotesDialog: React.FC<IReleaseNotesDialog> = ({
|
|||||||
<MarkdownPage page={n} key={i} />
|
<MarkdownPage page={n} key={i} />
|
||||||
))}
|
))}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
|||||||
import { useMutation, DocumentNode } from "@apollo/client";
|
import { useMutation, DocumentNode } from "@apollo/client";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { getStashboxBase } from "src/utils";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
|||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
icon={faPaperPlane}
|
icon={faPaperPlane}
|
||||||
header={intl.formatMessage({ id: "actions.submit_stash_box" })}
|
header={intl.formatMessage({ id: "actions.submit_stash_box" })}
|
||||||
isRunning={loading}
|
isRunning={loading}
|
||||||
@@ -153,7 +153,7 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
|||||||
<div>{error.message}</div>
|
<div>{error.message}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useConfigureUI } from "src/core/StashService";
|
import { useConfigureUI } from "src/core/StashService";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import { FrontPageConfig } from "./FrontPageConfig";
|
import { FrontPageConfig } from "./FrontPageConfig";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { Control } from "./Control";
|
import { Control } from "./Control";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||||
import { useFindSavedFilters } from "src/core/StashService";
|
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 { Button, Form, Modal } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
FilterMode,
|
FilterMode,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
|||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { useGalleryDestroy } from "src/core/StashService";
|
import { useGalleryDestroy } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -119,7 +119,7 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faTrashAlt}
|
icon={faTrashAlt}
|
||||||
header={header}
|
header={header}
|
||||||
@@ -155,6 +155,6 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
|||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { useBulkGalleryUpdate } from "src/core/StashService";
|
import { useBulkGalleryUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StudioSelect, Modal } from "src/components/Shared";
|
import { StudioSelect } from "../Shared/Select";
|
||||||
import { useToast } from "src/hooks";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { FormUtils } from "src/utils";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import FormUtils from "src/utils/form";
|
||||||
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
@@ -31,10 +32,8 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating100, setRating] = useState<number>();
|
const [rating100, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [
|
const [performerMode, setPerformerMode] =
|
||||||
performerMode,
|
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||||
setPerformerMode,
|
|
||||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
|
||||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||||
@@ -228,7 +227,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faPencilAlt}
|
icon={faPencilAlt}
|
||||||
header={intl.formatMessage(
|
header={intl.formatMessage(
|
||||||
@@ -302,7 +301,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import React from "react";
|
|||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Helmet } from "react-helmet";
|
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 { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import Gallery from "./GalleryDetails/Gallery";
|
import Gallery from "./GalleryDetails/Gallery";
|
||||||
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
||||||
import { GalleryList } from "./GalleryList";
|
import { GalleryList } from "./GalleryList";
|
||||||
|
|
||||||
const Galleries = () => {
|
const Galleries: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const title_template = `${intl.formatMessage({
|
const title_template = `${intl.formatMessage({
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ import { Button, ButtonGroup } from "react-bootstrap";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import { GridCard } from "../Shared/GridCard";
|
||||||
GridCard,
|
import { HoverPopover } from "../Shared/HoverPopover";
|
||||||
HoverPopover,
|
import { Icon } from "../Shared/Icon";
|
||||||
Icon,
|
import { TagLink } from "../Shared/TagLink";
|
||||||
TagLink,
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
TruncatedText,
|
|
||||||
} from "src/components/Shared";
|
|
||||||
import { PopoverCountButton } from "src/components/Shared/PopoverCountButton";
|
|
||||||
import { NavUtils } from "src/utils";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
|
||||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
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 { RatingBanner } from "../Shared/RatingBanner";
|
||||||
import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
|
import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import {
|
|||||||
useFindGallery,
|
useFindGallery,
|
||||||
useGalleryUpdate,
|
useGalleryUpdate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
ErrorMessage,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
LoadingIndicator,
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
Icon,
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
Counter,
|
|
||||||
} from "src/components/Shared";
|
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||||
import { GalleryDetailPanel } from "./GalleryDetailPanel";
|
import { GalleryDetailPanel } from "./GalleryDetailPanel";
|
||||||
@@ -214,7 +212,6 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
|||||||
<Tab.Pane eventKey="gallery-edit-panel">
|
<Tab.Pane eventKey="gallery-edit-panel">
|
||||||
<GalleryEditPanel
|
<GalleryEditPanel
|
||||||
isVisible={activeTabKey === "gallery-edit-panel"}
|
isVisible={activeTabKey === "gallery-edit-panel"}
|
||||||
isNew={false}
|
|
||||||
gallery={gallery}
|
gallery={gallery}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { ImageList } from "src/components/Images/ImageList";
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
import { showWhenSelected } from "src/hooks/ListHook";
|
import { showWhenSelected } from "src/hooks/ListHook";
|
||||||
import { mutateAddGalleryImages } from "src/core/StashService";
|
import { mutateAddGalleryImages } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||||
|
|
||||||
const GalleryCreate: React.FC = () => {
|
const GalleryCreate: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const location = useLocation();
|
||||||
function useQuery() {
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
const { search } = useLocation();
|
const gallery = {
|
||||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
title: query.get("q") ?? undefined,
|
||||||
}
|
};
|
||||||
|
|
||||||
const query = useQuery();
|
|
||||||
const nameQuery = query.get("name");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row new-view">
|
<div className="row new-view">
|
||||||
@@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => {
|
|||||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<GalleryEditPanel
|
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
|
||||||
isNew
|
|
||||||
gallery={{ title: nameQuery ?? "" }}
|
|
||||||
isVisible
|
|
||||||
onDelete={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import React from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import TextUtils from "src/utils/text";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { sortPerformers } from "src/core/performers";
|
import { sortPerformers } from "src/core/performers";
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ import {
|
|||||||
TagSelect,
|
TagSelect,
|
||||||
SceneSelect,
|
SceneSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
Icon,
|
} from "src/components/Shared/Select";
|
||||||
LoadingIndicator,
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
URLField,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
} from "src/components/Shared";
|
import { URLField } from "src/components/Shared/URLField";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { FormUtils } from "src/utils";
|
import FormUtils from "src/utils/form";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -40,23 +40,16 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INewProps {
|
export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
isNew: true;
|
gallery,
|
||||||
gallery?: Partial<GQL.GalleryDataFragment>;
|
isVisible,
|
||||||
}
|
onDelete,
|
||||||
|
}) => {
|
||||||
interface IExistingProps {
|
|
||||||
isNew: false;
|
|
||||||
gallery: GQL.GalleryDataFragment;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GalleryEditPanel: React.FC<
|
|
||||||
IProps & (INewProps | IExistingProps)
|
|
||||||
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -67,15 +60,14 @@ export const GalleryEditPanel: React.FC<
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isNew = gallery.id === undefined;
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
const Scrapers = useListGalleryScrapers();
|
const Scrapers = useListGalleryScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
|
|
||||||
const [
|
const [scrapedGallery, setScrapedGallery] =
|
||||||
scrapedGallery,
|
useState<GQL.ScrapedGallery | null>();
|
||||||
setScrapedGallery,
|
|
||||||
] = useState<GQL.ScrapedGallery | null>();
|
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { Accordion, Button, Card } from "react-bootstrap";
|
import { Accordion, Button, Card } from "react-bootstrap";
|
||||||
import { FormattedMessage, FormattedTime } from "react-intl";
|
import { FormattedMessage, FormattedTime } from "react-intl";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog";
|
import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { mutateGallerySetPrimaryFile } from "src/core/StashService";
|
import { mutateGallerySetPrimaryFile } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { TextUtils } from "src/utils";
|
import TextUtils from "src/utils/text";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
import { TextField, URLField } from "src/utils/field";
|
||||||
|
|
||||||
interface IFileInfoPanelProps {
|
interface IFileInfoPanelProps {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { ImageList } from "src/components/Images/ImageList";
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
import { mutateRemoveGalleryImages } from "src/core/StashService";
|
import { mutateRemoveGalleryImages } from "src/core/StashService";
|
||||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
@@ -17,7 +20,7 @@ import {
|
|||||||
useTagCreate,
|
useTagCreate,
|
||||||
makePerformerCreateInput,
|
makePerformerCreateInput,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import {
|
|||||||
FindGalleriesQueryResult,
|
FindGalleriesQueryResult,
|
||||||
SlimGalleryDataFragment,
|
SlimGalleryDataFragment,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { useGalleriesList } from "src/hooks";
|
import {
|
||||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
showWhenSelected,
|
||||||
|
PersistanceLevel,
|
||||||
|
useGalleriesList,
|
||||||
|
} from "src/hooks/ListHook";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { queryFindGalleries } from "src/core/StashService";
|
import { queryFindGalleries } from "src/core/StashService";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
import { useFindGalleries } from "src/core/StashService";
|
import { useFindGalleries } from "src/core/StashService";
|
||||||
import Slider from "react-slick";
|
import Slider from "@ant-design/react-slick";
|
||||||
import { GalleryCard } from "./GalleryCard";
|
import { GalleryCard } from "./GalleryCard";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||||
@@ -13,9 +13,7 @@ interface IProps {
|
|||||||
header: string;
|
header: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
|
export const GalleryRecommendationRow: React.FC<IProps> = (props) => {
|
||||||
props: IProps
|
|
||||||
) => {
|
|
||||||
const result = useFindGalleries(props.filter);
|
const result = useFindGalleries(props.filter);
|
||||||
const cardCount = result.data?.findGalleries.count;
|
const cardCount = result.data?.findGalleries.count;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useLightbox } from "src/hooks";
|
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import "flexbin/flexbin.css";
|
import "flexbin/flexbin.css";
|
||||||
import {
|
import {
|
||||||
CriterionModifier,
|
CriterionModifier,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React from "react";
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { TextUtils } from "src/utils";
|
import TextUtils from "src/utils/text";
|
||||||
import { useGalleryLightbox } from "src/hooks";
|
import { useGalleryLightbox } from "src/hooks/Lightbox/hooks";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "sass:math";
|
||||||
|
|
||||||
.gallery-image {
|
.gallery-image {
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -138,14 +140,14 @@ $galleryTabWidth: 450px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin galleryWidth($width) {
|
@mixin galleryWidth($width) {
|
||||||
height: ($width / 3) * 2;
|
height: math.div($width, 3) * 2;
|
||||||
|
|
||||||
&-landscape {
|
&-landscape {
|
||||||
width: $width;
|
width: $width;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-portrait {
|
&-portrait {
|
||||||
width: $width / 2;
|
width: math.div($width, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,9 +218,17 @@ $galleryTabWidth: 450px;
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.star-fill-10 .unfilled-star,
|
||||||
|
.star-fill-20 .unfilled-star,
|
||||||
.star-fill-25 .unfilled-star,
|
.star-fill-25 .unfilled-star,
|
||||||
|
.star-fill-30 .unfilled-star,
|
||||||
|
.star-fill-40 .unfilled-star,
|
||||||
.star-fill-50 .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;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,11 +173,9 @@ export const Manual: React.FC<IManualProps> = ({
|
|||||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
) {
|
) {
|
||||||
if (event.target instanceof HTMLAnchorElement) {
|
if (event.target instanceof HTMLAnchorElement) {
|
||||||
const href = (event.target as HTMLAnchorElement).getAttribute("href");
|
const href = event.target.getAttribute("href");
|
||||||
if (href && href.startsWith("/help")) {
|
if (href && href.startsWith("/help")) {
|
||||||
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
|
const newKey = event.target.pathname.substring("/help/".length);
|
||||||
"/help/".length
|
|
||||||
);
|
|
||||||
setActiveTab(newKey);
|
setActiveTab(newKey);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
|||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { useImagesDestroy } from "src/core/StashService";
|
import { useImagesDestroy } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -112,7 +112,7 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faTrashAlt}
|
icon={faTrashAlt}
|
||||||
header={header}
|
header={header}
|
||||||
@@ -146,6 +146,6 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
|||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { useBulkImageUpdate } from "src/core/StashService";
|
import { useBulkImageUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StudioSelect, Modal } from "src/components/Shared";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { useToast } from "src/hooks";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { FormUtils } from "src/utils";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import FormUtils from "src/utils/form";
|
||||||
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputIDs,
|
getAggregateInputIDs,
|
||||||
@@ -31,10 +32,8 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating100, setRating] = useState<number>();
|
const [rating100, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
const [
|
const [performerMode, setPerformerMode] =
|
||||||
performerMode,
|
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||||
setPerformerMode,
|
|
||||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
|
||||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||||
@@ -218,7 +217,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faPencilAlt}
|
icon={faPencilAlt}
|
||||||
header={intl.formatMessage(
|
header={intl.formatMessage(
|
||||||
@@ -292,7 +291,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import React, { MouseEvent, useMemo } from "react";
|
|||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
import { GridCard } from "../Shared/GridCard";
|
import { HoverPopover } from "src/components/Shared/HoverPopover";
|
||||||
import { RatingBanner } from "../Shared/RatingBanner";
|
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 {
|
import {
|
||||||
faBox,
|
faBox,
|
||||||
faImages,
|
faImages,
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import {
|
|||||||
useImageUpdate,
|
useImageUpdate,
|
||||||
mutateMetadataScan,
|
mutateMetadataScan,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
ErrorMessage,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
LoadingIndicator,
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
Icon,
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
Counter,
|
import { useToast } from "src/hooks/Toast";
|
||||||
} from "src/components/Shared";
|
|
||||||
import { useToast } from "src/hooks";
|
|
||||||
import * as Mousetrap from "mousetrap";
|
import * as Mousetrap from "mousetrap";
|
||||||
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
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("a", () => setActiveTabKey("image-details-panel"));
|
||||||
Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel"));
|
Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel"));
|
||||||
Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel"));
|
Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel"));
|
||||||
Mousetrap.bind("o", () => onIncrementClick());
|
Mousetrap.bind("o", () => {
|
||||||
|
onIncrementClick();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("a");
|
Mousetrap.unbind("a");
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import TextUtils from "src/utils/text";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { sortPerformers } from "src/core/performers";
|
import { sortPerformers } from "src/core/performers";
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
TagSelect,
|
TagSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
LoadingIndicator,
|
} from "src/components/Shared/Select";
|
||||||
URLField,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
} from "src/components/Shared";
|
import { URLField } from "src/components/Shared/URLField";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { FormUtils } from "src/utils";
|
import FormUtils from "src/utils/form";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Accordion, Button, Card } from "react-bootstrap";
|
import { Accordion, Button, Card } from "react-bootstrap";
|
||||||
import { FormattedMessage, FormattedNumber, FormattedTime } from "react-intl";
|
import { FormattedMessage, FormattedNumber, FormattedTime } from "react-intl";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog";
|
import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { mutateImageSetPrimaryFile } from "src/core/StashService";
|
import { mutateImageSetPrimaryFile } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { TextUtils } from "src/utils";
|
import TextUtils from "src/utils/text";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
import { TextField, URLField } from "src/utils/field";
|
||||||
|
|
||||||
interface IFileInfoPanelProps {
|
interface IFileInfoPanelProps {
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import {
|
|||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { queryFindImages } from "src/core/StashService";
|
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 { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import {
|
import {
|
||||||
IListHookOperation,
|
IListHookOperation,
|
||||||
showWhenSelected,
|
showWhenSelected,
|
||||||
PersistanceLevel,
|
PersistanceLevel,
|
||||||
|
useImagesList,
|
||||||
} from "src/hooks/ListHook";
|
} from "src/hooks/ListHook";
|
||||||
|
|
||||||
import { ImageCard } from "./ImageCard";
|
import { ImageCard } from "./ImageCard";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
import { useFindImages } from "src/core/StashService";
|
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 { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||||
import { RecommendationRow } from "../FrontPage/RecommendationRow";
|
import { RecommendationRow } from "../FrontPage/RecommendationRow";
|
||||||
@@ -13,9 +13,7 @@ interface IProps {
|
|||||||
header: string;
|
header: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageRecommendationRow: FunctionComponent<IProps> = (
|
export const ImageRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
||||||
props: IProps
|
|
||||||
) => {
|
|
||||||
const result = useFindImages(props.filter);
|
const result = useFindImages(props.filter);
|
||||||
const cardCount = result.data?.findImages.count;
|
const cardCount = result.data?.findImages.count;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Helmet } from "react-helmet";
|
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 { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { Image } from "./ImageDetails/Image";
|
import { Image } from "./ImageDetails/Image";
|
||||||
import { ImageList } from "./ImageList";
|
import { ImageList } from "./ImageList";
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { InputFilter } from "./Filters/InputFilter";
|
|||||||
import { DateFilter } from "./Filters/DateFilter";
|
import { DateFilter } from "./Filters/DateFilter";
|
||||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
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 { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids";
|
||||||
import { StashIDFilter } from "./Filters/StashIDFilter";
|
import { StashIDFilter } from "./Filters/StashIDFilter";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
CriterionValue,
|
CriterionValue,
|
||||||
} from "src/models/list-filter/criteria/criterion";
|
} from "src/models/list-filter/criteria/criterion";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "../Shared";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
interface IFilterTagsProps {
|
interface IFilterTagsProps {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { DurationInput } from "../../Shared";
|
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||||
import { INumberValue } from "../../../models/list-filter/types";
|
import { INumberValue } from "src/models/list-filter/types";
|
||||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
interface IDurationFilterProps {
|
interface IDurationFilterProps {
|
||||||
criterion: Criterion<INumberValue>;
|
criterion: Criterion<INumberValue>;
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ interface IHierarchicalLabelValueFilterProps {
|
|||||||
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HierarchicalLabelValueFilter: React.FC<IHierarchicalLabelValueFilterProps> = ({
|
export const HierarchicalLabelValueFilter: React.FC<
|
||||||
criterion,
|
IHierarchicalLabelValueFilterProps
|
||||||
onValueChanged,
|
> = ({ criterion, onValueChanged }) => {
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import {
|
|||||||
Overlay,
|
Overlay,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
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 { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { Icon } from "../Shared";
|
import { Icon } from "../Shared/Icon";
|
||||||
import {
|
import {
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faPencilAlt,
|
faPencilAlt,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "../Shared";
|
import { Icon } from "../Shared/Icon";
|
||||||
import {
|
import {
|
||||||
faList,
|
faList,
|
||||||
faSquare,
|
faSquare,
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import {
|
|||||||
useSaveFilter,
|
useSaveFilter,
|
||||||
useSetDefaultFilter,
|
useSetDefaultFilter,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { SavedFilterDataFragment } from "src/core/generated-graphql";
|
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 { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Icon } from "../Shared";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
interface ISavedFilterListProps {
|
interface ISavedFilterListProps {
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ import { LinkContainer } from "react-router-bootstrap";
|
|||||||
import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
|
import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
|
||||||
import { SessionUtils } from "src/utils";
|
import SessionUtils from "src/utils/session";
|
||||||
import Icon from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { ManualStateContext } from "./Help/context";
|
import { ManualStateContext } from "./Help/context";
|
||||||
import { SettingsButton } from "./SettingsButton";
|
import { SettingsButton } from "./SettingsButton";
|
||||||
import {
|
import {
|
||||||
faBars,
|
faBars,
|
||||||
faChartBar,
|
faChartColumn,
|
||||||
faFilm,
|
faFilm,
|
||||||
faHeart,
|
faHeart,
|
||||||
faImage,
|
faImage,
|
||||||
@@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => {
|
|||||||
|
|
||||||
const pathname = location.pathname.replace(/\/$/, "");
|
const pathname = location.pathname.replace(/\/$/, "");
|
||||||
let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;
|
let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;
|
||||||
if (newPath != null) {
|
if (newPath !== null) {
|
||||||
let queryParam = new URLSearchParams(location.search).get("q");
|
let queryParam = new URLSearchParams(location.search).get("q");
|
||||||
if (queryParam != null) {
|
if (queryParam) {
|
||||||
newPath += "?name=" + encodeURIComponent(queryParam);
|
newPath += "?q=" + encodeURIComponent(queryParam);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => {
|
|||||||
className="minimal d-flex align-items-center h-100"
|
className="minimal d-flex align-items-center h-100"
|
||||||
title={intl.formatMessage({ id: "statistics" })}
|
title={intl.formatMessage({ id: "statistics" })}
|
||||||
>
|
>
|
||||||
<Icon icon={faChartBar} />
|
<Icon icon={faChartColumn} />
|
||||||
</Button>
|
</Button>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { Form, Col, Row } from "react-bootstrap";
|
|||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useBulkMovieUpdate } from "src/core/StashService";
|
import { useBulkMovieUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal, StudioSelect } from "src/components/Shared";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { useToast } from "src/hooks";
|
import { StudioSelect } from "../Shared/Select";
|
||||||
import { FormUtils } from "src/utils";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
import FormUtils from "src/utils/form";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -100,7 +101,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faPencilAlt}
|
icon={faPencilAlt}
|
||||||
header={intl.formatMessage(
|
header={intl.formatMessage(
|
||||||
@@ -158,7 +159,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import { GridCard } from "../Shared/GridCard";
|
||||||
GridCard,
|
import { HoverPopover } from "../Shared/HoverPopover";
|
||||||
HoverPopover,
|
import { Icon } from "../Shared/Icon";
|
||||||
Icon,
|
import { TagLink } from "../Shared/TagLink";
|
||||||
TagLink,
|
import { TruncatedText } from "../Shared/TruncatedText";
|
||||||
TruncatedText,
|
|
||||||
} from "src/components/Shared";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { RatingBanner } from "../Shared/RatingBanner";
|
import { RatingBanner } from "../Shared/RatingBanner";
|
||||||
import { faPlayCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faPlayCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -20,7 +18,7 @@ interface IProps {
|
|||||||
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
||||||
function maybeRenderSceneNumber() {
|
function maybeRenderSceneNumber() {
|
||||||
if (!props.sceneIndex) return;
|
if (!props.sceneIndex) return;
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ import {
|
|||||||
useMovieDestroy,
|
useMovieDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
import {
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
DetailsEditNavbar,
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
ErrorMessage,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
LoadingIndicator,
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
Modal,
|
import { useToast } from "src/hooks/Toast";
|
||||||
} from "src/components/Shared";
|
|
||||||
import { useToast } from "src/hooks";
|
|
||||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||||
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
||||||
import { MovieEditPanel } from "./MovieEditPanel";
|
import { MovieEditPanel } from "./MovieEditPanel";
|
||||||
@@ -51,7 +49,9 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("e", () => setIsEditing(true));
|
Mousetrap.bind("e", () => setIsEditing(true));
|
||||||
Mousetrap.bind("d d", () => onDelete());
|
Mousetrap.bind("d d", () => {
|
||||||
|
onDelete();
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("e");
|
Mousetrap.unbind("e");
|
||||||
@@ -109,7 +109,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
|
|
||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
icon={faTrashAlt}
|
icon={faTrashAlt}
|
||||||
accept={{
|
accept={{
|
||||||
@@ -129,7 +129,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useMovieCreate } from "src/core/StashService";
|
import { useMovieCreate } from "src/core/StashService";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { MovieEditPanel } from "./MovieEditPanel";
|
import { MovieEditPanel } from "./MovieEditPanel";
|
||||||
|
|
||||||
const MovieCreate: React.FC = () => {
|
const MovieCreate: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
|
const movie = {
|
||||||
|
name: query.get("q") ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
// Editing movie state
|
// Editing movie state
|
||||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
const [frontImage, setFrontImage] = useState<string | null>();
|
||||||
undefined
|
const [backImage, setBackImage] = useState<string | null>();
|
||||||
);
|
|
||||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [createMovie] = useMovieCreate();
|
const [createMovie] = useMovieCreate();
|
||||||
@@ -84,6 +86,7 @@ const MovieCreate: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MovieEditPanel
|
<MovieEditPanel
|
||||||
|
movie={movie}
|
||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onCancel={() => history.push("/movies")}
|
onCancel={() => history.push("/movies")}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
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 { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
import { TextField, URLField } from "src/utils/field";
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import {
|
|||||||
queryScrapeMovieURL,
|
queryScrapeMovieURL,
|
||||||
useListMovieScrapers,
|
useListMovieScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
LoadingIndicator,
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
StudioSelect,
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
DetailsEditNavbar,
|
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||||
DurationInput,
|
import { URLField } from "src/components/Shared/URLField";
|
||||||
URLField,
|
import { useToast } from "src/hooks/Toast";
|
||||||
} from "src/components/Shared";
|
|
||||||
import { useToast } from "src/hooks";
|
|
||||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
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 { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
@@ -25,7 +25,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
|
||||||
interface IMovieEditPanel {
|
interface IMovieEditPanel {
|
||||||
movie?: Partial<GQL.MovieDataFragment>;
|
movie: Partial<GQL.MovieDataFragment>;
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||||
) => void;
|
) => void;
|
||||||
@@ -49,19 +49,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
const isNew = movie === undefined;
|
const isNew = movie.id === undefined;
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
const [imageClipboard, setImageClipboard] = useState<string>();
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListMovieScrapers();
|
||||||
const [scrapedMovie, setScrapedMovie] = useState<
|
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||||
GQL.ScrapedMovie | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
@@ -113,10 +109,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
setBackImage(formik.values.back_image);
|
setBackImage(formik.values.back_image);
|
||||||
}, [formik.values.back_image, setBackImage]);
|
}, [formik.values.back_image, setBackImage]);
|
||||||
|
|
||||||
useEffect(() => onImageEncoding(encodingImage), [
|
useEffect(
|
||||||
onImageEncoding,
|
() => onImageEncoding(encodingImage),
|
||||||
encodingImage,
|
[onImageEncoding, encodingImage]
|
||||||
]);
|
);
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
ScrapeDialogRow,
|
ScrapeDialogRow,
|
||||||
ScrapedTextAreaRow,
|
ScrapedTextAreaRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog";
|
||||||
import { StudioSelect } from "src/components/Shared";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { DurationUtils } from "src/utils";
|
import DurationUtils from "src/utils/duration";
|
||||||
import { useStudioCreate } from "src/core/StashService";
|
import { useStudioCreate } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
useMoviesList,
|
useMoviesList,
|
||||||
PersistanceLevel,
|
PersistanceLevel,
|
||||||
} from "src/hooks/ListHook";
|
} 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 { MovieCard } from "./MovieCard";
|
||||||
import { EditMoviesDialog } from "./EditMoviesDialog";
|
import { EditMoviesDialog } from "./EditMoviesDialog";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useFindMovies } from "src/core/StashService";
|
import { useFindMovies } from "src/core/StashService";
|
||||||
import Slider from "react-slick";
|
import Slider from "@ant-design/react-slick";
|
||||||
import { MovieCard } from "./MovieCard";
|
import { MovieCard } from "./MovieCard";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Helmet } from "react-helmet";
|
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 Movie from "./MovieDetails/Movie";
|
||||||
import MovieCreate from "./MovieDetails/MovieCreate";
|
import MovieCreate from "./MovieDetails/MovieCreate";
|
||||||
import { MovieList } from "./MovieList";
|
import { MovieList } from "./MovieList";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
import React from "react";
|
||||||
|
|
||||||
export const PageNotFound: FunctionComponent = () => {
|
export const PageNotFound: React.FC = () => {
|
||||||
return <h1>Page not found.</h1>;
|
return <h1>Page not found.</h1>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Col, Form, Row } from "react-bootstrap";
|
|||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { ModalComponent } from "../Shared/Modal";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import MultiSet from "../Shared/MultiSet";
|
import { MultiSet } from "../Shared/MultiSet";
|
||||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||||
import {
|
import {
|
||||||
getAggregateInputValue,
|
getAggregateInputValue,
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FormUtils } from "../../utils";
|
import FormUtils from "src/utils/form";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
selected: GQL.SlimPerformerDataFragment[];
|
selected: GQL.SlimPerformerDataFragment[];
|
||||||
@@ -60,10 +60,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
mode: GQL.BulkUpdateIdMode.Add,
|
mode: GQL.BulkUpdateIdMode.Add,
|
||||||
});
|
});
|
||||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||||
const [
|
const [aggregateState, setAggregateState] =
|
||||||
aggregateState,
|
useState<GQL.BulkPerformerUpdateInput>({});
|
||||||
setAggregateState,
|
|
||||||
] = useState<GQL.BulkPerformerUpdateInput>({});
|
|
||||||
// weight needs conversion to/from number
|
// weight needs conversion to/from number
|
||||||
const [weight, setWeight] = useState<string | undefined>();
|
const [weight, setWeight] = useState<string | undefined>();
|
||||||
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||||
@@ -183,7 +181,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<ModalComponent
|
||||||
show
|
show
|
||||||
icon={faPencilAlt}
|
icon={faPencilAlt}
|
||||||
header={intl.formatMessage(
|
header={intl.formatMessage(
|
||||||
@@ -319,7 +317,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import React from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import NavUtils from "src/utils/navigation";
|
||||||
import {
|
import TextUtils from "src/utils/text";
|
||||||
GridCard,
|
import { GridCard } from "../Shared/GridCard";
|
||||||
CountryFlag,
|
import { CountryFlag } from "../Shared/CountryFlag";
|
||||||
HoverPopover,
|
import { HoverPopover } from "../Shared/HoverPopover";
|
||||||
Icon,
|
import { Icon } from "../Shared/Icon";
|
||||||
TagLink,
|
import { TagLink } from "../Shared/TagLink";
|
||||||
} from "src/components/Shared";
|
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
@@ -19,6 +18,8 @@ import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
|||||||
import GenderIcon from "./GenderIcon";
|
import GenderIcon from "./GenderIcon";
|
||||||
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { RatingBanner } from "../Shared/RatingBanner";
|
import { RatingBanner } from "../Shared/RatingBanner";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { usePerformerUpdate } from "src/core/StashService";
|
||||||
|
|
||||||
export interface IPerformerCardExtraCriteria {
|
export interface IPerformerCardExtraCriteria {
|
||||||
scenes: Criterion<CriterionValue>[];
|
scenes: Criterion<CriterionValue>[];
|
||||||
@@ -61,17 +62,39 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
{ age, years_old: ageL10String }
|
{ age, years_old: ageL10String }
|
||||||
);
|
);
|
||||||
|
|
||||||
function maybeRenderFavoriteIcon() {
|
const [updatePerformer] = usePerformerUpdate();
|
||||||
if (performer.favorite === false) {
|
|
||||||
return;
|
function renderFavoriteIcon() {
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="favorite">
|
<Link to="" onClick={(e) => e.preventDefault()}>
|
||||||
|
<Button
|
||||||
|
className={cx(
|
||||||
|
"minimal",
|
||||||
|
"mousetrap",
|
||||||
|
"favorite-button",
|
||||||
|
performer.favorite ? "favorite" : "not-favorite"
|
||||||
|
)}
|
||||||
|
onClick={() => onToggleFavorite!(!performer.favorite)}
|
||||||
|
>
|
||||||
<Icon icon={faHeart} size="2x" />
|
<Icon icon={faHeart} size="2x" />
|
||||||
</div>
|
</Button>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onToggleFavorite(v: boolean) {
|
||||||
|
if (performer.id) {
|
||||||
|
updatePerformer({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: performer.id,
|
||||||
|
favorite: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function maybeRenderScenesPopoverButton() {
|
function maybeRenderScenesPopoverButton() {
|
||||||
if (!performer.scene_count) return;
|
if (!performer.scene_count) return;
|
||||||
|
|
||||||
@@ -214,7 +237,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
alt={performer.name ?? ""}
|
alt={performer.name ?? ""}
|
||||||
src={performer.image_path ?? ""}
|
src={performer.image_path ?? ""}
|
||||||
/>
|
/>
|
||||||
{maybeRenderFavoriteIcon()}
|
|
||||||
|
{renderFavoriteIcon()}
|
||||||
{maybeRenderRatingBanner()}
|
{maybeRenderRatingBanner()}
|
||||||
{maybeRenderFlag()}
|
{maybeRenderFlag()}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,17 +12,16 @@ import {
|
|||||||
usePerformerDestroy,
|
usePerformerDestroy,
|
||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
Counter,
|
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||||
CountryFlag,
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
DetailsEditNavbar,
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
ErrorMessage,
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
Icon,
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
LoadingIndicator,
|
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||||
} from "src/components/Shared";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useLightbox, useToast } from "src/hooks";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
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 { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
@@ -32,12 +31,8 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
|||||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||||
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
||||||
import GenderIcon from "../GenderIcon";
|
import GenderIcon from "../GenderIcon";
|
||||||
import {
|
import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||||
faCamera,
|
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||||
faDove,
|
|
||||||
faHeart,
|
|
||||||
faLink,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { IUIConfig } from "src/core/config";
|
import { IUIConfig } from "src/core/config";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
|
|
||||||
@@ -247,7 +242,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible={isEditing}
|
isVisible={isEditing}
|
||||||
isNew={false}
|
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
onImageEncoding={onImageEncoding}
|
onImageEncoding={onImageEncoding}
|
||||||
onCancelEditing={() => {
|
onCancelEditing={() => {
|
||||||
@@ -351,7 +345,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Icon icon={faDove} />
|
<Icon icon={faTwitter} />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -366,7 +360,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<Icon icon={faCamera} />
|
<Icon icon={faInstagram} />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -405,7 +399,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
<h2>
|
<h2>
|
||||||
<GenderIcon
|
<GenderIcon
|
||||||
gender={performer.gender}
|
gender={performer.gender}
|
||||||
className="gender-icon mr-2 flag-icon"
|
className="gender-icon mr-2 fi"
|
||||||
/>
|
/>
|
||||||
<CountryFlag country={performer.country} className="mr-2" />
|
<CountryFlag country={performer.country} className="mr-2" />
|
||||||
<span className="performer-name">{performer.name}</span>
|
<span className="performer-name">{performer.name}</span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
@@ -8,13 +8,11 @@ const PerformerCreate: React.FC = () => {
|
|||||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||||
|
|
||||||
function useQuery() {
|
const location = useLocation();
|
||||||
const { search } = useLocation();
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
const performer = {
|
||||||
}
|
name: query.get("q") ?? undefined,
|
||||||
|
};
|
||||||
const query = useQuery();
|
|
||||||
const nameQuery = query.get("name");
|
|
||||||
|
|
||||||
const activeImage = imagePreview ?? "";
|
const activeImage = imagePreview ?? "";
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -50,9 +48,8 @@ const PerformerCreate: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={{ name: nameQuery ?? "" }}
|
performer={performer}
|
||||||
isVisible
|
isVisible
|
||||||
isNew
|
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
onImageEncoding={onImageEncoding}
|
onImageEncoding={onImageEncoding}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user