Merge branch 'develop' into releases/0.19.1

This commit is contained in:
WithoutPants
2023-02-21 15:56:42 +11:00
committed by GitHub
307 changed files with 7906 additions and 6572 deletions

1
.gitignore vendored
View File

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

View File

@@ -3,8 +3,11 @@ https://stashapp.cc
[![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml) [![Build](https://github.com/stashapp/stash/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/stashapp/stash/actions/workflows/build.yml)
[![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/stash 'DockerHub') [![Docker pulls](https://img.shields.io/docker/pulls/stashapp/stash.svg)](https://hub.docker.com/r/stashapp/stash 'DockerHub')
[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp)
[![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash) [![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash)
[![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt) [![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest)
[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty)
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.** ### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
![demo image](docs/readme_assets/demo_image.png) ![demo image](docs/readme_assets/demo_image.png)

1
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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
} }

View File

@@ -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 {

View File

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

View File

@@ -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
} }

View File

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

View File

@@ -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 {

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -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
} }
} }

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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{})

View File

@@ -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,

View File

@@ -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))]
} }

View File

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

View File

@@ -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
View 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
}

View File

@@ -1,3 +1,2 @@
BROWSER=none BROWSER=none
PORT=3000
ESLINT_NO_DEV_ERRORS=true ESLINT_NO_DEV_ERRORS=true

View File

@@ -9,61 +9,69 @@
"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:import/recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:import/recommended", "plugin:react/jsx-runtime",
"prettier", "airbnb/hooks",
"prettier/prettier" "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": [
"error", "error",
{ {
"selector": "interface", "selector": "interface",
"format": ["PascalCase"], "format": ["PascalCase"],
"custom": { "custom": {
"regex": "^I[A-Z]", "regex": "^I[A-Z]",
"match": true "match": true
}
} }
], }
"lines-between-class-members": "off", ],
"@typescript-eslint/lines-between-class-members": "off", "lines-between-class-members": "off",
"import/extensions": [ "@typescript-eslint/lines-between-class-members": "off",
"error", "import/extensions": [
"ignorePackages", "error",
{ "ignorePackages",
"js": "never", {
"jsx": "never", "js": "never",
"ts": "never", "jsx": "never",
"tsx": "never" "ts": "never",
} "tsx": "never"
], }
"import/named": "off", ],
"import/namespace": "off", "import/named": "off",
"import/no-unresolved": "off", "import/namespace": "off",
"react/display-name": "off", "import/no-unresolved": "off",
"react/prop-types": "off", "react/display-name": "off",
"react/style-prop-object": ["error", { "react/prop-types": "off",
"react/style-prop-object": [
"error",
{
"allow": ["FormattedNumber"] "allow": ["FormattedNumber"]
}], }
"spaced-comment": ["error", "always", { ],
"markers": ["/"] "spaced-comment": [
}], "error",
"prefer-destructuring": ["error", {"object": true, "array": false}], "always",
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }], {
"no-nested-ternary": "off" "markers": ["/"]
}
],
"prefer-destructuring": ["error", { "object": true, "array": false }],
"@typescript-eslint/no-use-before-define": [
"error",
{ "functions": false, "classes": true }
],
"no-nested-ternary": "off"
} }
} }

5
ui/v2.5/.gitignore vendored
View File

@@ -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
View 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

View File

@@ -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"
},
} }

View File

@@ -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}/*"
}
}
]
}

View File

@@ -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"
]
}

View File

@@ -4,11 +4,9 @@ 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
- typescript-react-apollo - typescript-react-apollo
config: config:
withRefetchFn: true withRefetchFn: true

View File

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

View File

@@ -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
View 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;
}
}
}

View 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;
}

View File

@@ -1,111 +1,111 @@
/* 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. *
* * There are two error codes that can be reported back currently:
* There are two error codes that can be reported back currently: * * 0 BadSignature
* * 0 BadSignature * * 1 BadTimeStamp
* * 1 BadTimeStamp *
* * Note: Exceptions other then ParsingError will be thrown and not reported.
* Note: Exceptions other then ParsingError will be thrown and not reported. */
*/ class ParsingError extends Error {
class ParsingError extends Error { readonly name: string;
readonly name: string; readonly code: number;
readonly code: number; readonly message: string;
readonly message: string;
}
namespace WebVTT {
/**
* A parser for the WebVTT spec in JavaScript.
*/
class Parser {
/**
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
*
* @param window the window object to use
* @param vttjs the vtt.js module
* @param decoder the decoder to decode `parse()` data with
*/
constructor(window: Window);
constructor(window: Window, decoder: TextDecoder);
constructor(window: Window, vttjs: vttjs, decoder: TextDecoder);
/**
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
*/
onregion?: (cue: VTTRegion) => void;
/**
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
*/
oncue?: (cue: VTTCue) => void;
/**
* Is invoked in response to `flush()` and after the content was parsed completely.
*/
onflush?: () => void;
/**
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
* Is passed a `ParsingError` object.
*/
onparsingerror?: (e: ParsingError) => void;
/**
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
*
* @param data data to be parsed
*/
parse(data: string): this;
/**
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
* Will also trigger `onflush`.
*/
flush(): this;
}
/**
* Helper to allow strings to be decoded instead of the default binary utf8 data.
*/
function StringDecoder(): TextDecoder;
/**
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
*
* @param window window object to use
* @param cuetext cue text to parse
*/
function convertCueToDOMTree(
window: Window,
cuetext: string
): HTMLDivElement | null;
/**
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
*
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
*/
function processCues(
window: Window,
cues: VTTCue[],
overlay: Element
): void;
}
} }
export = vttjs; export namespace WebVTT {
/**
* A parser for the WebVTT spec in JavaScript.
*/
class Parser {
/**
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
*
* @param window the window object to use
* @param vttjs the vtt.js module
* @param decoder the decoder to decode `parse()` data with
*/
constructor(window: Window);
constructor(window: Window, decoder: TextDecoder);
constructor(
window: Window,
vttjs: typeof import("videojs-vtt.js"),
decoder: TextDecoder
);
/**
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
*/
onregion?: (cue: VTTRegion) => void;
/**
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
*/
oncue?: (cue: VTTCue) => void;
/**
* Is invoked in response to `flush()` and after the content was parsed completely.
*/
onflush?: () => void;
/**
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
* Is passed a `ParsingError` object.
*/
onparsingerror?: (e: ParsingError) => void;
/**
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
*
* @param data data to be parsed
*/
parse(data: string): this;
/**
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
* Will also trigger `onflush`.
*/
flush(): this;
}
/**
* Helper to allow strings to be decoded instead of the default binary utf8 data.
*/
function StringDecoder(): TextDecoder;
/**
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
*
* @param window window object to use
* @param cuetext cue text to parse
*/
function convertCueToDOMTree(
window: Window,
cuetext: string
): HTMLDivElement | null;
/**
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
*
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
*/
function processCues(
window: Window,
cues: VTTCue[],
overlay: Element
): void;
}
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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>
); );
}; };

View File

@@ -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(() => {

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View File

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

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
} }

View File

@@ -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({

View File

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

View File

@@ -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)}
/> />

View File

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

View File

@@ -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>
); );

View File

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

View File

@@ -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);

View File

@@ -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 {

View File

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

View File

@@ -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>,

View File

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

View File

@@ -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;

View File

@@ -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,

View File

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

View File

@@ -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;
} }

View File

@@ -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();
} }

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
} }

View File

@@ -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,

View File

@@ -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");

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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;

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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>;

View File

@@ -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 (

View File

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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

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

View File

@@ -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>
); );
} }

View File

@@ -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;

View File

@@ -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>
); );
} }

View File

@@ -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={() => {}}

View File

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

View File

@@ -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);

View File

@@ -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>,

View File

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

View File

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

View File

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

View File

@@ -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>;
}; };

View File

@@ -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>
); );
} }

View File

@@ -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()}>
<Icon icon={faHeart} size="2x" /> <Button
</div> className={cx(
"minimal",
"mousetrap",
"favorite-button",
performer.favorite ? "favorite" : "not-favorite"
)}
onClick={() => onToggleFavorite!(!performer.favorite)}
>
<Icon icon={faHeart} size="2x" />
</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()}
</> </>

View File

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

View File

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