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

|
||||
|
||||
1
go.mod
1
go.mod
@@ -58,6 +58,7 @@ require (
|
||||
github.com/vearutop/statigz v1.1.6
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.1
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@@ -760,6 +760,8 @@ github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rty
|
||||
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
|
||||
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
|
||||
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -16,7 +18,7 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (ret *models
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Gallery.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
@@ -23,7 +25,7 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
||||
}
|
||||
|
||||
image, err = qb.Find(ctx, idInt)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
} else if checksum != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -16,7 +18,7 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Movie.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -34,7 +36,6 @@ func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.Movi
|
||||
Count: total,
|
||||
Movies: movies,
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -16,7 +18,7 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Performer.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -16,7 +18,7 @@ func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *mo
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SavedFilter.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
return ret, err
|
||||
@@ -40,7 +42,7 @@ func (r *queryResolver) FindDefaultFilter(ctx context.Context, mode models.Filte
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SavedFilter.FindDefault(ctx, mode)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
return ret, err
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
@@ -21,7 +23,7 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
|
||||
return err
|
||||
}
|
||||
scene, err = qb.Find(ctx, idInt)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
} else if checksum != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -17,7 +19,7 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (ret *models.
|
||||
var err error
|
||||
ret, err = r.repository.Studio.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -16,7 +18,7 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.Find(ctx, idInt)
|
||||
return err
|
||||
}); err != nil {
|
||||
}); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -339,6 +339,9 @@ func serveFiles(w http.ResponseWriter, r *http.Request, name string, paths []str
|
||||
}
|
||||
}
|
||||
|
||||
// Always revalidate with server
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
|
||||
bufferReader := bytes.NewReader(buffer.Bytes())
|
||||
http.ServeContent(w, r, name, latestModTime, bufferReader)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration); err != nil {
|
||||
if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration, videoFile.FrameRate); err != nil {
|
||||
logger.Errorf("error generating preview: %v", err)
|
||||
logErrorOutput(err)
|
||||
return
|
||||
@@ -59,12 +59,18 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64) error {
|
||||
func (t GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64, videoFrameRate float64) error {
|
||||
videoFilename := t.Scene.Path
|
||||
useVsync2 := false
|
||||
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false); err != nil {
|
||||
if videoFrameRate <= 0.01 {
|
||||
logger.Errorf("[generator] Video framerate very low/high (%f) most likely vfr so using -vsync 2", videoFrameRate)
|
||||
useVsync2 = true
|
||||
}
|
||||
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false, useVsync2); err != nil {
|
||||
logger.Warnf("[generator] failed generating scene preview, trying fallback")
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true); err != nil {
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true, useVsync2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/remeh/sizedwaitgroup"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -574,7 +575,7 @@ func (s *scanJob) handleFile(ctx context.Context, f scanFile) error {
|
||||
// scan zip files with a different context that is not cancellable
|
||||
// cancelling while scanning zip file contents results in the scan
|
||||
// contents being partially completed
|
||||
zipCtx := context.Background()
|
||||
zipCtx := utils.ValueOnlyContext{Context: ctx}
|
||||
|
||||
if err := s.scanZipFile(zipCtx, f); err != nil {
|
||||
logger.Errorf("Error scanning zip file %q: %v", f.Path, err)
|
||||
|
||||
@@ -2,11 +2,18 @@ package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/xWTF/chardet"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -40,6 +47,42 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Concat all Name and Comment for better detection result
|
||||
var buffer bytes.Buffer
|
||||
for _, f := range zipReader.File {
|
||||
buffer.WriteString(f.Name)
|
||||
buffer.WriteString(f.Comment)
|
||||
}
|
||||
buffer.WriteString(zipReader.Comment)
|
||||
|
||||
// Detect encoding
|
||||
d, err := chardet.NewTextDetector().DetectBest(buffer.Bytes())
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
return nil, fmt.Errorf("unable to detect decoding: %w", err)
|
||||
}
|
||||
|
||||
// If the charset is not UTF8, decode'em
|
||||
if d.Charset != "UTF-8" {
|
||||
logger.Debugf("Detected non-utf8 zip charset %s (%s): %s", d.Charset, d.Language, path)
|
||||
|
||||
e, _ := charset.Lookup(d.Charset)
|
||||
if e == nil {
|
||||
reader.Close()
|
||||
return nil, fmt.Errorf("failed to lookup charset %s, language %s", d.Charset, d.Language)
|
||||
}
|
||||
|
||||
decoder := e.NewDecoder()
|
||||
for _, f := range zipReader.File {
|
||||
f.Name, _, err = transform.String(decoder, f.Name)
|
||||
if err != nil {
|
||||
reader.Close()
|
||||
return nil, fmt.Errorf("failed to decode %v: %w", []byte(f.Name), err)
|
||||
}
|
||||
// Comments are not decoded cuz stash doesn't use that
|
||||
}
|
||||
}
|
||||
|
||||
return &ZipFS{
|
||||
Reader: zipReader,
|
||||
zipFileCloser: reader,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type valueOnlyContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (valueOnlyContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (valueOnlyContext) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (valueOnlyContext) Err() error {
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
const maxGraveyardSize = 10
|
||||
@@ -178,7 +179,7 @@ func (m *Manager) dispatch(ctx context.Context, j *Job) (done chan struct{}) {
|
||||
j.StartTime = &t
|
||||
j.Status = StatusRunning
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(valueOnlyContext{ctx})
|
||||
ctx, cancelFunc := context.WithCancel(utils.ValueOnlyContext{Context: ctx})
|
||||
j.cancelFunc = cancelFunc
|
||||
|
||||
done = make(chan struct{})
|
||||
|
||||
@@ -68,7 +68,7 @@ func (g PreviewOptions) getStepSizeAndOffset(videoDuration float64) (stepSize fl
|
||||
return
|
||||
}
|
||||
|
||||
func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool) error {
|
||||
func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool, useVsync2 bool) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
@@ -81,7 +81,7 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration
|
||||
|
||||
logger.Infof("[generator] generating video preview for %s", input)
|
||||
|
||||
if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback)); err != nil {
|
||||
if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback, useVsync2)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn {
|
||||
func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
// #2496 - generate a single preview video for videos shorter than segments * segment duration
|
||||
if videoDuration < options.SegmentDuration*float64(options.Segments) {
|
||||
return g.previewVideoSingle(input, videoDuration, options, fallback)
|
||||
return g.previewVideoSingle(input, videoDuration, options, fallback, useVsync2)
|
||||
}
|
||||
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
@@ -131,7 +131,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr
|
||||
Preset: options.Preset,
|
||||
}
|
||||
|
||||
if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback); err != nil {
|
||||
if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool) generateFn {
|
||||
func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
chunkOptions := previewChunkOptions{
|
||||
StartTime: 0,
|
||||
@@ -160,7 +160,7 @@ func (g *Generator) previewVideoSingle(input string, videoDuration float64, opti
|
||||
Preset: options.Preset,
|
||||
}
|
||||
|
||||
return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback)
|
||||
return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ type previewChunkOptions struct {
|
||||
Preset string
|
||||
}
|
||||
|
||||
func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool) error {
|
||||
func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool, useVsync2 bool) error {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleWidth(scenePreviewWidth)
|
||||
|
||||
@@ -189,6 +189,10 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt
|
||||
"-strict", "-2",
|
||||
)
|
||||
|
||||
if useVsync2 {
|
||||
videoArgs = append(videoArgs, "-vsync", "2")
|
||||
}
|
||||
|
||||
trimOptions := transcoder.TranscodeOptions{
|
||||
OutputPath: options.OutputPath,
|
||||
StartTime: options.StartTime,
|
||||
|
||||
@@ -69,7 +69,7 @@ var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123
|
||||
|
||||
func randomSequence(n int) string {
|
||||
b := make([]rune, n)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := range b {
|
||||
b[i] = characters[rand.Intn(len(characters))]
|
||||
}
|
||||
|
||||
@@ -707,6 +707,13 @@ func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.Scen
|
||||
ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images)
|
||||
}
|
||||
|
||||
if ss.URL == nil && len(s.Urls) > 0 {
|
||||
// The scene in Stash-box may not have a Studio URL but it does have another URL.
|
||||
// For example it has a www.manyvids.com URL, which is auto set as type ManyVids.
|
||||
// This should be re-visited once Stashapp can support more than one URL.
|
||||
ss.URL = &s.Urls[0].URL
|
||||
}
|
||||
|
||||
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
||||
pqb := c.repository.Performer
|
||||
tqb := c.repository.Tag
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -1706,5 +1707,26 @@ func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int) ([][]*mo
|
||||
}
|
||||
}
|
||||
|
||||
sortByPath(duplicates)
|
||||
|
||||
return duplicates, nil
|
||||
}
|
||||
|
||||
func sortByPath(scenes [][]*models.Scene) {
|
||||
lessFunc := func(i int, j int) bool {
|
||||
firstPathI := getFirstPath(scenes[i])
|
||||
firstPathJ := getFirstPath(scenes[j])
|
||||
return firstPathI < firstPathJ
|
||||
}
|
||||
sort.SliceStable(scenes, lessFunc)
|
||||
}
|
||||
|
||||
func getFirstPath(scenes []*models.Scene) string {
|
||||
var firstPath string
|
||||
for i, scene := range scenes {
|
||||
if i == 0 || scene.Path < firstPath {
|
||||
firstPath = scene.Path
|
||||
}
|
||||
}
|
||||
return firstPath
|
||||
}
|
||||
|
||||
22
pkg/utils/context.go
Normal file
22
pkg/utils/context.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ValueOnlyContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (ValueOnlyContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (ValueOnlyContext) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ValueOnlyContext) Err() error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
BROWSER=none
|
||||
PORT=3000
|
||||
ESLINT_NO_DEV_ERRORS=true
|
||||
|
||||
@@ -9,61 +9,69 @@
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"jsx-a11y"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "jsx-a11y"],
|
||||
"extends": [
|
||||
"airbnb-typescript",
|
||||
"airbnb/hooks",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/recommended",
|
||||
"prettier",
|
||||
"prettier/prettier"
|
||||
"airbnb-typescript",
|
||||
"plugin:import/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"airbnb/hooks",
|
||||
"prettier"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": 2,
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"@typescript-eslint/no-explicit-any": 2,
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"custom": {
|
||||
"regex": "^I[A-Z]",
|
||||
"match": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"import/named": "off",
|
||||
"import/namespace": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/style-prop-object": ["error", {
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"import/named": "off",
|
||||
"import/namespace": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/style-prop-object": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["FormattedNumber"]
|
||||
}],
|
||||
"spaced-comment": ["error", "always", {
|
||||
"markers": ["/"]
|
||||
}],
|
||||
"prefer-destructuring": ["error", {"object": true, "array": false}],
|
||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }],
|
||||
"no-nested-ternary": "off"
|
||||
}
|
||||
],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": ["/"]
|
||||
}
|
||||
],
|
||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
"error",
|
||||
{ "functions": false, "classes": true }
|
||||
],
|
||||
"no-nested-ternary": "off"
|
||||
}
|
||||
}
|
||||
|
||||
5
ui/v2.5/.gitignore
vendored
5
ui/v2.5/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# generated
|
||||
src/core/generated-*.tsx
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@@ -12,6 +13,7 @@
|
||||
/build
|
||||
|
||||
# misc
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
@@ -23,3 +25,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
18
ui/v2.5/.prettierignore
Normal file
18
ui/v2.5/.prettierignore
Normal file
@@ -0,0 +1,18 @@
|
||||
*.md
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# locales
|
||||
src/locales/**/*.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# generated
|
||||
src/core/generated-graphql.tsx
|
||||
@@ -1,91 +1,55 @@
|
||||
{
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
"extends": "stylelint-config-prettier",
|
||||
"plugins": ["stylelint-order"],
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"indentation": null,
|
||||
"at-rule-empty-line-before": [ "always", {
|
||||
except: ["after-same-name", "first-nested" ],
|
||||
ignore: ["after-comment"],
|
||||
} ],
|
||||
"at-rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["after-same-name", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"selector-no-vendor-prefix": true,
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-after": "always-single-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": [ "always", {
|
||||
except: ["first-nested"],
|
||||
ignore: ["stylelint-commands"],
|
||||
} ],
|
||||
"comment-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["stylelint-commands"]
|
||||
}
|
||||
],
|
||||
"comment-whitespace-inside": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"declaration-no-important": true,
|
||||
"font-family-name-quotes": "always-where-recommended",
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-newline-after": "always-multi-line",
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-url-quotes": "always",
|
||||
"function-whitespace-after": "always",
|
||||
"length-zero-no-unit": true,
|
||||
"max-empty-lines": 1,
|
||||
"max-nesting-depth": 4,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"media-query-list-comma-newline-after": "always-multi-line",
|
||||
"media-query-list-comma-space-after": "always-single-line",
|
||||
"media-query-list-comma-space-before": "never",
|
||||
"media-feature-parentheses-space-inside": "never",
|
||||
"no-descending-specificity": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-max-precision": 3,
|
||||
"number-no-trailing-zeros": true,
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
"declarations"
|
||||
],
|
||||
"order/order": ["custom-properties", "declarations"],
|
||||
"order/properties-alphabetical-order": true,
|
||||
"rule-empty-line-before": ["always-multi-line", {
|
||||
except: ["after-single-line-comment", "first-nested" ],
|
||||
ignore: ["after-comment"],
|
||||
}],
|
||||
"rule-empty-line-before": [
|
||||
"always-multi-line",
|
||||
{
|
||||
"except": ["after-single-line-comment", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"selector-max-id": 1,
|
||||
"selector-max-type": 2,
|
||||
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-max-universal": 0,
|
||||
"selector-type-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"string-no-newline": true,
|
||||
"string-quotes": "double",
|
||||
"time-min-milliseconds": 100,
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never"
|
||||
},
|
||||
"time-min-milliseconds": 100
|
||||
}
|
||||
}
|
||||
|
||||
18
ui/v2.5/.vscode/launch.json
vendored
18
ui/v2.5/.vscode/launch.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Chrome",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///src/*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
26
ui/v2.5/.vscode/settings.json
vendored
26
ui/v2.5/.vscode/settings.json
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.tabSize": 2,
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.wordWrap": "bounded",
|
||||
"javascript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"editor.wordWrapColumn": 120,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en-GB",
|
||||
"spellright.language": [
|
||||
"en"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"latex",
|
||||
"plaintext",
|
||||
"typescriptreact"
|
||||
]
|
||||
}
|
||||
@@ -4,11 +4,9 @@ documents: "../../graphql/documents/**/*.graphql"
|
||||
generates:
|
||||
src/core/generated-graphql.tsx:
|
||||
plugins:
|
||||
- add:
|
||||
content: "/* eslint-disable */"
|
||||
- time
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typescript-react-apollo
|
||||
config:
|
||||
withRefetchFn: true
|
||||
withRefetchFn: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="/">
|
||||
<base href="/" />
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
@@ -10,27 +10,15 @@
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<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" />
|
||||
<title>Stash</title>
|
||||
<script>window.STASH_BASE_URL = "/%BASE_URL%/"</script>
|
||||
<script>
|
||||
window.STASH_BASE_URL = "/%BASE_URL%/";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,125 +3,116 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "./",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build-ci": "yarn validate && yarn build",
|
||||
"validate": "yarn lint && tsc --noEmit && yarn format-check",
|
||||
"lint": "yarn lint:css && yarn lint:js",
|
||||
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
||||
"lint:css": "stylelint \"src/**/*.scss\"",
|
||||
"format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
||||
"format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
||||
"build-ci": "yarn run validate && yarn run build",
|
||||
"validate": "yarn run lint && yarn run check && yarn run format-check",
|
||||
"lint": "yarn run lint:css && yarn run lint:js",
|
||||
"lint:css": "stylelint --cache \"src/**/*.scss\"",
|
||||
"lint:js": "eslint --cache src/",
|
||||
"check": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"gqlgen": "gql-gen --config codegen.yml",
|
||||
"extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
">0.5% and supports es6-module-dynamic-import"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.7",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.5.3",
|
||||
"@formatjs/intl-locale": "^2.4.14",
|
||||
"@formatjs/intl-numberformat": "^6.1.3",
|
||||
"@formatjs/intl-pluralrules": "^4.0.6",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@types/react-select": "^4.0.8",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"apollo-upload-client": "^14.1.3",
|
||||
"axios": "^1.1.3",
|
||||
"@ant-design/react-slick": "^1.0.0",
|
||||
"@apollo/client": "^3.7.8",
|
||||
"@formatjs/intl-getcanonicallocales": "^2.0.5",
|
||||
"@formatjs/intl-locale": "^3.0.11",
|
||||
"@formatjs/intl-numberformat": "^8.3.3",
|
||||
"@formatjs/intl-pluralrules": "^5.1.8",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"axios": "^1.3.3",
|
||||
"base64-blob": "^1.4.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"classnames": "^2.2.6",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"bootstrap": "^4.6.2",
|
||||
"classnames": "^2.3.2",
|
||||
"flag-icons": "^6.6.6",
|
||||
"flexbin": "^0.2.0",
|
||||
"formik": "^2.2.6",
|
||||
"graphql": "^15.4.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"i18n-iso-countries": "^6.4.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"localforage": "^1.9.0",
|
||||
"formik": "^2.2.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.3",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mousetrap-pause": "^1.0.0",
|
||||
"normalize-url": "^4.5.1",
|
||||
"postcss": "^8.2.10",
|
||||
"query-string": "6.13.8",
|
||||
"react": "17.0.2",
|
||||
"react-bootstrap": "1.4.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.6.6",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.10.16",
|
||||
"react-markdown": "^7.1.0",
|
||||
"react-intl": "^6.2.8",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-router-bootstrap": "^0.25.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-hash-link": "^2.3.1",
|
||||
"react-select": "^4.0.2",
|
||||
"react-slick": "^0.29.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-select": "^5.7.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.32.5",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"string.prototype.replaceall": "^1.0.4",
|
||||
"subscriptions-transport-ws": "^0.9.18",
|
||||
"string.prototype.replaceall": "^1.0.7",
|
||||
"thehandy": "^1.0.3",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"video.js": "^7.20.3",
|
||||
"video.js": "^7.21.1",
|
||||
"videojs-mobile-ui": "^0.8.0",
|
||||
"videojs-seek-buttons": "^3.0.1",
|
||||
"videojs-vtt.js": "^0.15.4",
|
||||
"vite": "^2.9.13",
|
||||
"vite-plugin-compression": "^0.3.5",
|
||||
"vite-tsconfig-paths": "^3.3.17",
|
||||
"ws": "^7.4.6",
|
||||
"yup": "^0.32.9"
|
||||
"yup": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^2.0.2",
|
||||
"@graphql-codegen/cli": "^1.20.0",
|
||||
"@graphql-codegen/time": "^2.0.2",
|
||||
"@graphql-codegen/typescript": "^1.20.00",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.13",
|
||||
"@graphql-codegen/typescript-react-apollo": "^2.2.1",
|
||||
"@types/apollo-upload-client": "^14.1.0",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/fslightbox-react": "^1.4.0",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@graphql-codegen/cli": "^3.0.0",
|
||||
"@graphql-codegen/time": "^4.0.0",
|
||||
"@graphql-codegen/typescript": "^3.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/mousetrap": "^1.6.5",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/react": "17.0.31",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"@types/react-helmet": "^6.1.3",
|
||||
"@types/mousetrap": "^1.6.11",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/react": "^17.0.53",
|
||||
"@types/react-dom": "^17.0.19",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-router-bootstrap": "^0.24.5",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/react-router-hash-link": "^1.2.1",
|
||||
"@types/react-slick": "^0.23.8",
|
||||
"@types/video.js": "^7.3.49",
|
||||
"@types/react-router-hash-link": "^2.4.5",
|
||||
"@types/video.js": "^7.3.51",
|
||||
"@types/videojs-mobile-ui": "^0.5.0",
|
||||
"@types/videojs-seek-buttons": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^14.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"postcss-safe-parser": "^5.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"stylelint": "^13.9.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-order": "^4.1.0",
|
||||
"typescript": "~4.4.4"
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.8.4",
|
||||
"sass": "^1.58.1",
|
||||
"stylelint": "^15.1.0",
|
||||
"stylelint-order": "^6.0.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.8.4",
|
||||
"vite": "^4.1.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-tsconfig-paths": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "mousetrap-pause" {
|
||||
import { MousetrapStatic } from "mousetrap";
|
||||
|
||||
function MousetrapPause(mousetrap: MousetrapStatic): MousetrapStatic;
|
||||
|
||||
export default MousetrapPause;
|
||||
|
||||
module "mousetrap" {
|
||||
interface MousetrapStatic {
|
||||
pause(): void;
|
||||
unpause(): void;
|
||||
pauseCombo(combo: string): void;
|
||||
unpauseCombo(combo: string): void;
|
||||
}
|
||||
interface MousetrapInstance {
|
||||
pause(): void;
|
||||
unpause(): void;
|
||||
pauseCombo(combo: string): void;
|
||||
unpauseCombo(combo: string): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
declare module "string.prototype.replaceall" {
|
||||
function replaceAll(
|
||||
searchValue: string | RegExp,
|
||||
replaceValue: string
|
||||
): string;
|
||||
function replaceAll(
|
||||
searchValue: string | RegExp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
replacer: (substring: string, ...args: any[]) => string
|
||||
): string;
|
||||
|
||||
namespace replaceAll {
|
||||
function getPolyfill(): typeof replaceAll;
|
||||
function implementation(): typeof replaceAll;
|
||||
function shim(): void;
|
||||
}
|
||||
|
||||
export default replaceAll;
|
||||
}
|
||||
210
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
210
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
@@ -1,111 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "videojs-vtt.js" {
|
||||
namespace vttjs {
|
||||
/**
|
||||
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
|
||||
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
|
||||
*
|
||||
* There are two error codes that can be reported back currently:
|
||||
* * 0 BadSignature
|
||||
* * 1 BadTimeStamp
|
||||
*
|
||||
* Note: Exceptions other then ParsingError will be thrown and not reported.
|
||||
*/
|
||||
class ParsingError extends Error {
|
||||
readonly name: string;
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
namespace WebVTT {
|
||||
/**
|
||||
* A parser for the WebVTT spec in JavaScript.
|
||||
*/
|
||||
class Parser {
|
||||
/**
|
||||
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
|
||||
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
|
||||
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
|
||||
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
|
||||
*
|
||||
* @param window the window object to use
|
||||
* @param vttjs the vtt.js module
|
||||
* @param decoder the decoder to decode `parse()` data with
|
||||
*/
|
||||
constructor(window: Window);
|
||||
constructor(window: Window, decoder: TextDecoder);
|
||||
constructor(window: Window, vttjs: vttjs, decoder: TextDecoder);
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
|
||||
*/
|
||||
onregion?: (cue: VTTRegion) => void;
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
|
||||
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
|
||||
*/
|
||||
oncue?: (cue: VTTCue) => void;
|
||||
|
||||
/**
|
||||
* Is invoked in response to `flush()` and after the content was parsed completely.
|
||||
*/
|
||||
onflush?: () => void;
|
||||
|
||||
/**
|
||||
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
|
||||
* Is passed a `ParsingError` object.
|
||||
*/
|
||||
onparsingerror?: (e: ParsingError) => void;
|
||||
|
||||
/**
|
||||
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
|
||||
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
|
||||
*
|
||||
* @param data data to be parsed
|
||||
*/
|
||||
parse(data: string): this;
|
||||
|
||||
/**
|
||||
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
|
||||
* Will also trigger `onflush`.
|
||||
*/
|
||||
flush(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to allow strings to be decoded instead of the default binary utf8 data.
|
||||
*/
|
||||
function StringDecoder(): TextDecoder;
|
||||
|
||||
/**
|
||||
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
|
||||
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
|
||||
*
|
||||
* @param window window object to use
|
||||
* @param cuetext cue text to parse
|
||||
*/
|
||||
function convertCueToDOMTree(
|
||||
window: Window,
|
||||
cuetext: string
|
||||
): HTMLDivElement | null;
|
||||
|
||||
/**
|
||||
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
|
||||
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
|
||||
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
|
||||
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
|
||||
*
|
||||
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
|
||||
*/
|
||||
function processCues(
|
||||
window: Window,
|
||||
cues: VTTCue[],
|
||||
overlay: Element
|
||||
): void;
|
||||
}
|
||||
/**
|
||||
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
|
||||
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
|
||||
*
|
||||
* There are two error codes that can be reported back currently:
|
||||
* * 0 BadSignature
|
||||
* * 1 BadTimeStamp
|
||||
*
|
||||
* Note: Exceptions other then ParsingError will be thrown and not reported.
|
||||
*/
|
||||
class ParsingError extends Error {
|
||||
readonly name: string;
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export = vttjs;
|
||||
export namespace WebVTT {
|
||||
/**
|
||||
* A parser for the WebVTT spec in JavaScript.
|
||||
*/
|
||||
class Parser {
|
||||
/**
|
||||
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
|
||||
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
|
||||
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
|
||||
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
|
||||
*
|
||||
* @param window the window object to use
|
||||
* @param vttjs the vtt.js module
|
||||
* @param decoder the decoder to decode `parse()` data with
|
||||
*/
|
||||
constructor(window: Window);
|
||||
constructor(window: Window, decoder: TextDecoder);
|
||||
constructor(
|
||||
window: Window,
|
||||
vttjs: typeof import("videojs-vtt.js"),
|
||||
decoder: TextDecoder
|
||||
);
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
|
||||
*/
|
||||
onregion?: (cue: VTTRegion) => void;
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
|
||||
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
|
||||
*/
|
||||
oncue?: (cue: VTTCue) => void;
|
||||
|
||||
/**
|
||||
* Is invoked in response to `flush()` and after the content was parsed completely.
|
||||
*/
|
||||
onflush?: () => void;
|
||||
|
||||
/**
|
||||
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
|
||||
* Is passed a `ParsingError` object.
|
||||
*/
|
||||
onparsingerror?: (e: ParsingError) => void;
|
||||
|
||||
/**
|
||||
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
|
||||
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
|
||||
*
|
||||
* @param data data to be parsed
|
||||
*/
|
||||
parse(data: string): this;
|
||||
|
||||
/**
|
||||
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
|
||||
* Will also trigger `onflush`.
|
||||
*/
|
||||
flush(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to allow strings to be decoded instead of the default binary utf8 data.
|
||||
*/
|
||||
function StringDecoder(): TextDecoder;
|
||||
|
||||
/**
|
||||
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
|
||||
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
|
||||
*
|
||||
* @param window window object to use
|
||||
* @param cuetext cue text to parse
|
||||
*/
|
||||
function convertCueToDOMTree(
|
||||
window: Window,
|
||||
cuetext: string
|
||||
): HTMLDivElement | null;
|
||||
|
||||
/**
|
||||
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
|
||||
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
|
||||
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
|
||||
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
|
||||
*
|
||||
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
|
||||
*/
|
||||
function processCues(
|
||||
window: Window,
|
||||
cues: VTTCue[],
|
||||
overlay: Element
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Helmet } from "react-helmet";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import mergeWith from "lodash-es/mergeWith";
|
||||
import { ToastProvider } from "src/hooks/Toast";
|
||||
import LightboxProvider from "src/hooks/Lightbox/context";
|
||||
import { LightboxProvider } from "src/hooks/Lightbox/context";
|
||||
import { initPolyfills } from "src/polyfills";
|
||||
|
||||
import locales, { registerCountry } from "src/locales";
|
||||
@@ -14,14 +14,15 @@ import {
|
||||
useConfigureUI,
|
||||
useSystemStatus,
|
||||
} from "src/core/StashService";
|
||||
import { flattenMessages } from "src/utils";
|
||||
import flattenMessages from "./utils/flattenMessages";
|
||||
import Mousetrap from "mousetrap";
|
||||
import MousetrapPause from "mousetrap-pause";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { MainNavbar } from "./components/MainNavbar";
|
||||
import { PageNotFound } from "./components/PageNotFound";
|
||||
import * as GQL from "./core/generated-graphql";
|
||||
import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared";
|
||||
import { TITLE_SUFFIX } from "./components/Shared/constants";
|
||||
import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
|
||||
|
||||
import { ConfigurationProvider } from "./hooks/Config";
|
||||
import { ManualProvider } from "./components/Help/context";
|
||||
@@ -91,10 +92,12 @@ export const App: React.FC = () => {
|
||||
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
||||
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
||||
const chosenMessages = (await locales[messageLanguage]()).default;
|
||||
const res = await fetch(getPlatformURL() + "customlocales");
|
||||
let customMessages = {};
|
||||
try {
|
||||
customMessages = res.ok ? await res.json() : {};
|
||||
const res = await fetch(getPlatformURL() + "customlocales");
|
||||
if (res.ok) {
|
||||
customMessages = await res.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useChangelogStorage } from "src/hooks";
|
||||
import { useChangelogStorage } from "src/hooks/LocalForage";
|
||||
import Version from "./Version";
|
||||
import V010 from "src/docs/en/Changelog/v010.md";
|
||||
import V011 from "src/docs/en/Changelog/v011.md";
|
||||
@@ -26,9 +26,6 @@ import V0180 from "src/docs/en/Changelog/v0180.md";
|
||||
import V0190 from "src/docs/en/Changelog/v0190.md";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
|
||||
// to avoid use of explicit any
|
||||
type Module = typeof V010;
|
||||
|
||||
const Changelog: React.FC = () => {
|
||||
const [{ data, loading }, setOpenState] = useChangelogStorage();
|
||||
|
||||
@@ -55,7 +52,7 @@ const Changelog: React.FC = () => {
|
||||
interface IStashRelease {
|
||||
version: string;
|
||||
date?: string;
|
||||
page: Module;
|
||||
page: string;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useState } from "react";
|
||||
import { Button, Card, Collapse } from "react-bootstrap";
|
||||
import { FormattedDate, FormattedMessage } from "react-intl";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
|
||||
interface IVersionProps {
|
||||
version: string;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Form, Button } from "react-bootstrap";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { Manual } from "../Help/Manual";
|
||||
import { withoutTypename } from "src/utils";
|
||||
import { withoutTypename } from "src/utils/data";
|
||||
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
||||
import { SettingSection } from "../Settings/SettingSection";
|
||||
import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -169,7 +170,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
modalProps={{ animation, size: "lg" }}
|
||||
icon={faCogs}
|
||||
@@ -203,7 +204,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
||||
/>
|
||||
</SettingSection>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Form, Button, Table } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
@@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC<IFieldOptionsList> = ({
|
||||
allowSetDefault = true,
|
||||
defaultOptions,
|
||||
}) => {
|
||||
const [localFieldOptions, setLocalFieldOptions] = useState<
|
||||
GQL.IdentifyFieldOptions[]
|
||||
>();
|
||||
const [localFieldOptions, setLocalFieldOptions] =
|
||||
useState<GQL.IdentifyFieldOptions[]>();
|
||||
const [editField, setEditField] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
useConfigureDefaults,
|
||||
useListSceneScrapers,
|
||||
} from "src/core/StashService";
|
||||
import { Icon, Modal, OperationButton } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { OperationButton } from "src/components/Shared/OperationButton";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { withoutTypename } from "src/utils";
|
||||
import { withoutTypename } from "src/utils/data";
|
||||
import {
|
||||
SCRAPER_PREFIX,
|
||||
STASH_BOX_PREFIX,
|
||||
@@ -202,9 +204,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
|
||||
if (s.options) {
|
||||
const sourceOptions = withoutTypename(s.options);
|
||||
sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map(
|
||||
withoutTypename
|
||||
);
|
||||
sourceOptions.fieldOptions =
|
||||
sourceOptions.fieldOptions?.map(withoutTypename);
|
||||
ret.options = sourceOptions;
|
||||
}
|
||||
|
||||
@@ -215,9 +216,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
setSources(mappedSources);
|
||||
if (identifyDefaults.options) {
|
||||
const defaultOptions = withoutTypename(identifyDefaults.options);
|
||||
defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map(
|
||||
withoutTypename
|
||||
);
|
||||
defaultOptions.fieldOptions =
|
||||
defaultOptions.fieldOptions?.map(withoutTypename);
|
||||
setOptions(defaultOptions);
|
||||
}
|
||||
} else {
|
||||
@@ -405,7 +405,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
modalProps={{ animation, size: "lg" }}
|
||||
show
|
||||
icon={faCogs}
|
||||
@@ -453,7 +453,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
setEditingField={(v) => setEditingField(v)}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Button, ListGroup } from "react-bootstrap";
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { IScraperSource } from "./constants";
|
||||
@@ -53,7 +54,7 @@ export const SourcesEditor: React.FC<ISourceEditor> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
dialogClassName="identify-source-editor"
|
||||
modalProps={{ animation: false, size: "lg" }}
|
||||
show
|
||||
@@ -107,7 +108,7 @@ export const SourcesEditor: React.FC<ISourceEditor> = ({
|
||||
defaultOptions={defaultOptions}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const sceneFields = [
|
||||
"tags",
|
||||
"stash_ids",
|
||||
] as const;
|
||||
export type SceneField = typeof sceneFields[number];
|
||||
export type SceneField = (typeof sceneFields)[number];
|
||||
|
||||
export const multiValueSceneFields: SceneField[] = [
|
||||
"studio",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { faCogs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useIntl } from "react-intl";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
import { Module } from "src/docs/en/ReleaseNotes";
|
||||
|
||||
interface IReleaseNotesDialog {
|
||||
notes: Module[];
|
||||
notes: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -18,7 +17,7 @@ export const ReleaseNotesDialog: React.FC<IReleaseNotesDialog> = ({
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faCogs}
|
||||
header={intl.formatMessage({ id: "release_notes" })}
|
||||
@@ -32,7 +31,7 @@ export const ReleaseNotesDialog: React.FC<IReleaseNotesDialog> = ({
|
||||
<MarkdownPage page={n} key={i} />
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
||||
import { useMutation, DocumentNode } from "@apollo/client";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { getStashboxBase } from "src/utils";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
@@ -78,7 +78,7 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
||||
undefined;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
icon={faPaperPlane}
|
||||
header={intl.formatMessage({ id: "actions.submit_stash_box" })}
|
||||
isRunning={loading}
|
||||
@@ -153,7 +153,7 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
||||
<div>{error.message}</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useConfigureUI } from "src/core/StashService";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FrontPageConfig } from "./FrontPageConfig";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { Control } from "./Control";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||
import { useFindSavedFilters } from "src/core/StashService";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import {
|
||||
FilterMode,
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useGalleryDestroy } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -119,7 +119,7 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faTrashAlt}
|
||||
header={header}
|
||||
@@ -155,6 +155,6 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { useBulkGalleryUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StudioSelect, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
@@ -31,10 +32,8 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
setPerformerMode,
|
||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerMode, setPerformerMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
@@ -228,7 +227,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
@@ -302,7 +301,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { TITLE_SUFFIX } from "src/components/Shared";
|
||||
import { TITLE_SUFFIX } from "../Shared/constants";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import Gallery from "./GalleryDetails/Gallery";
|
||||
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
||||
import { GalleryList } from "./GalleryList";
|
||||
|
||||
const Galleries = () => {
|
||||
const Galleries: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const title_template = `${intl.formatMessage({
|
||||
|
||||
@@ -2,17 +2,15 @@ import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
GridCard,
|
||||
HoverPopover,
|
||||
Icon,
|
||||
TagLink,
|
||||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import { PopoverCountButton } from "src/components/Shared/PopoverCountButton";
|
||||
import { NavUtils } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { GridCard } from "../Shared/GridCard";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import { faBox, faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
|
||||
@@ -9,14 +9,12 @@ import {
|
||||
useFindGallery,
|
||||
useGalleryUpdate,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
ErrorMessage,
|
||||
LoadingIndicator,
|
||||
Icon,
|
||||
Counter,
|
||||
} from "src/components/Shared";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { Counter } from "src/components/Shared/Counter";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
import { GalleryDetailPanel } from "./GalleryDetailPanel";
|
||||
@@ -214,7 +212,6 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
||||
<Tab.Pane eventKey="gallery-edit-panel">
|
||||
<GalleryEditPanel
|
||||
isVisible={activeTabKey === "gallery-edit-panel"}
|
||||
isNew={false}
|
||||
gallery={gallery}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { showWhenSelected } from "src/hooks/ListHook";
|
||||
import { mutateAddGalleryImages } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useIntl } from "react-intl";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
|
||||
const GalleryCreate: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const gallery = {
|
||||
title: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row new-view">
|
||||
@@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => {
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel
|
||||
isNew
|
||||
gallery={{ title: nameQuery ?? "" }}
|
||||
isVisible
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,9 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
TagSelect,
|
||||
SceneSelect,
|
||||
StudioSelect,
|
||||
Icon,
|
||||
LoadingIndicator,
|
||||
URLField,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
} from "src/components/Shared/Select";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { URLField } from "src/components/Shared/URLField";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useFormik } from "formik";
|
||||
import { FormUtils } from "src/utils";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -40,23 +40,16 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
isVisible: boolean;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
interface INewProps {
|
||||
isNew: true;
|
||||
gallery?: Partial<GQL.GalleryDataFragment>;
|
||||
}
|
||||
|
||||
interface IExistingProps {
|
||||
isNew: false;
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
export const GalleryEditPanel: React.FC<
|
||||
IProps & (INewProps | IExistingProps)
|
||||
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
||||
export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
gallery,
|
||||
isVisible,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
@@ -67,15 +60,14 @@ export const GalleryEditPanel: React.FC<
|
||||
}))
|
||||
);
|
||||
|
||||
const isNew = gallery.id === undefined;
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const Scrapers = useListGalleryScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [
|
||||
scrapedGallery,
|
||||
setScrapedGallery,
|
||||
] = useState<GQL.ScrapedGallery | null>();
|
||||
const [scrapedGallery, setScrapedGallery] =
|
||||
useState<GQL.ScrapedGallery | null>();
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Accordion, Button, Card } from "react-bootstrap";
|
||||
import { FormattedMessage, FormattedTime } from "react-intl";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateGallerySetPrimaryFile } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IFileInfoPanelProps {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { mutateRemoveGalleryImages } from "src/core/StashService";
|
||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { useIntl } from "react-intl";
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { StudioSelect, PerformerSelect } from "src/components/Shared";
|
||||
import {
|
||||
StudioSelect,
|
||||
PerformerSelect,
|
||||
TagSelect,
|
||||
} from "src/components/Shared/Select";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TagSelect } from "src/components/Shared/Select";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapeDialogRow,
|
||||
@@ -17,7 +20,7 @@ import {
|
||||
useTagCreate,
|
||||
makePerformerCreateInput,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
|
||||
@@ -8,8 +8,11 @@ import {
|
||||
FindGalleriesQueryResult,
|
||||
SlimGalleryDataFragment,
|
||||
} from "src/core/generated-graphql";
|
||||
import { useGalleriesList } from "src/hooks";
|
||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||
import {
|
||||
showWhenSelected,
|
||||
PersistanceLevel,
|
||||
useGalleriesList,
|
||||
} from "src/hooks/ListHook";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { queryFindGalleries } from "src/core/StashService";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindGalleries } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { GalleryCard } from "./GalleryCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
@@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const GalleryRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindGalleries(props.filter);
|
||||
const cardCount = result.data?.findGalleries.count;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useLightbox } from "src/hooks";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import "flexbin/flexbin.css";
|
||||
import {
|
||||
CriterionModifier,
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useGalleryLightbox } from "src/hooks";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { useGalleryLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "sass:math";
|
||||
|
||||
.gallery-image {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@@ -138,14 +140,14 @@ $galleryTabWidth: 450px;
|
||||
}
|
||||
|
||||
@mixin galleryWidth($width) {
|
||||
height: ($width / 3) * 2;
|
||||
height: math.div($width, 3) * 2;
|
||||
|
||||
&-landscape {
|
||||
width: $width;
|
||||
}
|
||||
|
||||
&-portrait {
|
||||
width: $width / 2;
|
||||
width: math.div($width, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,9 +218,17 @@ $galleryTabWidth: 450px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.star-fill-10 .unfilled-star,
|
||||
.star-fill-20 .unfilled-star,
|
||||
.star-fill-25 .unfilled-star,
|
||||
.star-fill-30 .unfilled-star,
|
||||
.star-fill-40 .unfilled-star,
|
||||
.star-fill-50 .unfilled-star,
|
||||
.star-fill-75 .unfilled-star {
|
||||
.star-fill-60 .unfilled-star,
|
||||
.star-fill-70 .unfilled-star,
|
||||
.star-fill-75 .unfilled-star,
|
||||
.star-fill-80 .unfilled-star,
|
||||
.star-fill-90 .unfilled-star {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -173,11 +173,9 @@ export const Manual: React.FC<IManualProps> = ({
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
const href = (event.target as HTMLAnchorElement).getAttribute("href");
|
||||
const href = event.target.getAttribute("href");
|
||||
if (href && href.startsWith("/help")) {
|
||||
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
|
||||
"/help/".length
|
||||
);
|
||||
const newKey = event.target.pathname.substring("/help/".length);
|
||||
setActiveTab(newKey);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useImagesDestroy } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -112,7 +112,7 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faTrashAlt}
|
||||
header={header}
|
||||
@@ -146,6 +146,6 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,11 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { useBulkImageUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StudioSelect, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputIDs,
|
||||
@@ -31,10 +32,8 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
setPerformerMode,
|
||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerMode, setPerformerMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
@@ -218,7 +217,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
@@ -292,7 +291,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ import React, { MouseEvent, useMemo } from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { GridCard } from "../Shared/GridCard";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { HoverPopover } from "src/components/Shared/HoverPopover";
|
||||
import { SweatDrops } from "src/components/Shared/SweatDrops";
|
||||
import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton";
|
||||
import { GridCard } from "src/components/Shared/GridCard";
|
||||
import { RatingBanner } from "src/components/Shared/RatingBanner";
|
||||
import {
|
||||
faBox,
|
||||
faImages,
|
||||
|
||||
@@ -11,13 +11,11 @@ import {
|
||||
useImageUpdate,
|
||||
mutateMetadataScan,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
ErrorMessage,
|
||||
LoadingIndicator,
|
||||
Icon,
|
||||
Counter,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { Counter } from "src/components/Shared/Counter";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import * as Mousetrap from "mousetrap";
|
||||
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||
@@ -239,7 +237,9 @@ export const Image: React.FC = () => {
|
||||
Mousetrap.bind("a", () => setActiveTabKey("image-details-panel"));
|
||||
Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel"));
|
||||
Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel"));
|
||||
Mousetrap.bind("o", () => onIncrementClick());
|
||||
Mousetrap.bind("o", () => {
|
||||
onIncrementClick();
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("a");
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { sortPerformers } from "src/core/performers";
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
PerformerSelect,
|
||||
TagSelect,
|
||||
StudioSelect,
|
||||
LoadingIndicator,
|
||||
URLField,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
} from "src/components/Shared/Select";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { URLField } from "src/components/Shared/URLField";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { Accordion, Button, Card } from "react-bootstrap";
|
||||
import { FormattedMessage, FormattedNumber, FormattedTime } from "react-intl";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import DeleteFilesDialog from "src/components/Shared/DeleteFilesDialog";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateImageSetPrimaryFile } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IFileInfoPanelProps {
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
} from "src/core/generated-graphql";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { queryFindImages } from "src/core/StashService";
|
||||
import { useImagesList, useLightbox } from "src/hooks";
|
||||
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import {
|
||||
IListHookOperation,
|
||||
showWhenSelected,
|
||||
PersistanceLevel,
|
||||
useImagesList,
|
||||
} from "src/hooks/ListHook";
|
||||
|
||||
import { ImageCard } from "./ImageCard";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindImages } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
import { RecommendationRow } from "../FrontPage/RecommendationRow";
|
||||
@@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const ImageRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const ImageRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
||||
const result = useFindImages(props.filter);
|
||||
const cardCount = result.data?.findImages.count;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { TITLE_SUFFIX } from "src/components/Shared";
|
||||
import { TITLE_SUFFIX } from "../Shared/constants";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { Image } from "./ImageDetails/Image";
|
||||
import { ImageList } from "./ImageList";
|
||||
|
||||
@@ -36,7 +36,7 @@ import { InputFilter } from "./Filters/InputFilter";
|
||||
import { DateFilter } from "./Filters/DateFilter";
|
||||
import { TimestampFilter } from "./Filters/TimestampFilter";
|
||||
import { CountryCriterion } from "src/models/list-filter/criteria/country";
|
||||
import { CountrySelect } from "../Shared";
|
||||
import { CountrySelect } from "../Shared/CountrySelect";
|
||||
import { StashIDCriterion } from "src/models/list-filter/criteria/stash-ids";
|
||||
import { StashIDFilter } from "./Filters/StashIDFilter";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
CriterionValue,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface IFilterTagsProps {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { CriterionModifier } from "../../../core/generated-graphql";
|
||||
import { DurationInput } from "../../Shared";
|
||||
import { INumberValue } from "../../../models/list-filter/types";
|
||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||
import { INumberValue } from "src/models/list-filter/types";
|
||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
|
||||
interface IDurationFilterProps {
|
||||
criterion: Criterion<INumberValue>;
|
||||
|
||||
@@ -10,10 +10,9 @@ interface IHierarchicalLabelValueFilterProps {
|
||||
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
||||
}
|
||||
|
||||
export const HierarchicalLabelValueFilter: React.FC<IHierarchicalLabelValueFilterProps> = ({
|
||||
criterion,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
export const HierarchicalLabelValueFilter: React.FC<
|
||||
IHierarchicalLabelValueFilterProps
|
||||
> = ({ criterion, onValueChanged }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (
|
||||
|
||||
@@ -17,9 +17,9 @@ import {
|
||||
Overlay,
|
||||
} from "react-bootstrap";
|
||||
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { useFocus } from "src/utils";
|
||||
import useFocus from "src/utils/focus";
|
||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import Mousetrap from "mousetrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { Icon } from "../Shared";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import {
|
||||
faEllipsisH,
|
||||
faPencilAlt,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "react-bootstrap";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import {
|
||||
faList,
|
||||
faSquare,
|
||||
|
||||
@@ -15,13 +15,13 @@ import {
|
||||
useSaveFilter,
|
||||
useSetDefaultFilter,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { SavedFilterDataFragment } from "src/core/generated-graphql";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface ISavedFilterListProps {
|
||||
|
||||
@@ -11,14 +11,14 @@ import { LinkContainer } from "react-router-bootstrap";
|
||||
import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
import { SessionUtils } from "src/utils";
|
||||
import Icon from "src/components/Shared/Icon";
|
||||
import SessionUtils from "src/utils/session";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { ManualStateContext } from "./Help/context";
|
||||
import { SettingsButton } from "./SettingsButton";
|
||||
import {
|
||||
faBars,
|
||||
faChartBar,
|
||||
faChartColumn,
|
||||
faFilm,
|
||||
faHeart,
|
||||
faImage,
|
||||
@@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => {
|
||||
|
||||
const pathname = location.pathname.replace(/\/$/, "");
|
||||
let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;
|
||||
if (newPath != null) {
|
||||
if (newPath !== null) {
|
||||
let queryParam = new URLSearchParams(location.search).get("q");
|
||||
if (queryParam != null) {
|
||||
newPath += "?name=" + encodeURIComponent(queryParam);
|
||||
if (queryParam) {
|
||||
newPath += "?q=" + encodeURIComponent(queryParam);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => {
|
||||
className="minimal d-flex align-items-center h-100"
|
||||
title={intl.formatMessage({ id: "statistics" })}
|
||||
>
|
||||
<Icon icon={faChartBar} />
|
||||
<Icon icon={faChartColumn} />
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
|
||||
@@ -3,9 +3,10 @@ import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useBulkMovieUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal, StudioSelect } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormUtils } from "src/utils";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { StudioSelect } from "../Shared/Select";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputValue,
|
||||
@@ -100,7 +101,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
@@ -158,7 +159,7 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
GridCard,
|
||||
HoverPopover,
|
||||
Icon,
|
||||
TagLink,
|
||||
TruncatedText,
|
||||
} from "src/components/Shared";
|
||||
import { GridCard } from "../Shared/GridCard";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import { faPlayCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -20,7 +18,7 @@ interface IProps {
|
||||
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
||||
export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
||||
function maybeRenderSceneNumber() {
|
||||
if (!props.sceneIndex) return;
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@ import {
|
||||
useMovieDestroy,
|
||||
} from "src/core/StashService";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import {
|
||||
DetailsEditNavbar,
|
||||
ErrorMessage,
|
||||
LoadingIndicator,
|
||||
Modal,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
@@ -51,7 +49,9 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("e");
|
||||
@@ -109,7 +109,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
|
||||
function renderDeleteAlert() {
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show={isDeleteAlertOpen}
|
||||
icon={faTrashAlt}
|
||||
accept={{
|
||||
@@ -129,7 +129,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useMovieCreate } from "src/core/StashService";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
|
||||
const MovieCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const Toast = useToast();
|
||||
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const movie = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
// Editing movie state
|
||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [frontImage, setFrontImage] = useState<string | null>();
|
||||
const [backImage, setBackImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [createMovie] = useMovieCreate();
|
||||
@@ -84,6 +86,7 @@ const MovieCreate: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<MovieEditPanel
|
||||
movie={movie}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => history.push("/movies")}
|
||||
onDelete={() => {}}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { DurationUtils, TextUtils } from "src/utils";
|
||||
import DurationUtils from "src/utils/duration";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
queryScrapeMovieURL,
|
||||
useListMovieScrapers,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
LoadingIndicator,
|
||||
StudioSelect,
|
||||
DetailsEditNavbar,
|
||||
DurationInput,
|
||||
URLField,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { DurationInput } from "src/components/Shared/DurationInput";
|
||||
import { URLField } from "src/components/Shared/URLField";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
||||
import { DurationUtils, FormUtils, ImageUtils } from "src/utils";
|
||||
import DurationUtils from "src/utils/duration";
|
||||
import FormUtils from "src/utils/form";
|
||||
import ImageUtils from "src/utils/image";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
@@ -25,7 +25,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IMovieEditPanel {
|
||||
movie?: Partial<GQL.MovieDataFragment>;
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
onSubmit: (
|
||||
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) => void;
|
||||
@@ -49,19 +49,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
const Toast = useToast();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const isNew = movie === undefined;
|
||||
const isNew = movie.id === undefined;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||
|
||||
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [imageClipboard, setImageClipboard] = useState<string>();
|
||||
|
||||
const Scrapers = useListMovieScrapers();
|
||||
const [scrapedMovie, setScrapedMovie] = useState<
|
||||
GQL.ScrapedMovie | undefined
|
||||
>();
|
||||
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
@@ -113,10 +109,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
setBackImage(formik.values.back_image);
|
||||
}, [formik.values.back_image, setBackImage]);
|
||||
|
||||
useEffect(() => onImageEncoding(encodingImage), [
|
||||
onImageEncoding,
|
||||
encodingImage,
|
||||
]);
|
||||
useEffect(
|
||||
() => onImageEncoding(encodingImage),
|
||||
[onImageEncoding, encodingImage]
|
||||
);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
ScrapeDialogRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
import { StudioSelect } from "src/components/Shared";
|
||||
import { DurationUtils } from "src/utils";
|
||||
import { StudioSelect } from "src/components/Shared/Select";
|
||||
import DurationUtils from "src/utils/duration";
|
||||
import { useStudioCreate } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
useMoviesList,
|
||||
PersistanceLevel,
|
||||
} from "src/hooks/ListHook";
|
||||
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||
import { ExportDialog } from "../Shared/ExportDialog";
|
||||
import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog";
|
||||
import { MovieCard } from "./MovieCard";
|
||||
import { EditMoviesDialog } from "./EditMoviesDialog";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useFindMovies } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { MovieCard } from "./MovieCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { TITLE_SUFFIX } from "src/components/Shared";
|
||||
import { TITLE_SUFFIX } from "src/components/Shared/constants";
|
||||
import Movie from "./MovieDetails/Movie";
|
||||
import MovieCreate from "./MovieDetails/MovieCreate";
|
||||
import { MovieList } from "./MovieList";
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ import { Col, Form, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import MultiSet from "../Shared/MultiSet";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { MultiSet } from "../Shared/MultiSet";
|
||||
import { RatingSystem } from "../Shared/Rating/RatingSystem";
|
||||
import {
|
||||
getAggregateInputValue,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox";
|
||||
import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
||||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FormUtils } from "../../utils";
|
||||
import FormUtils from "src/utils/form";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimPerformerDataFragment[];
|
||||
@@ -60,10 +60,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||
const [
|
||||
aggregateState,
|
||||
setAggregateState,
|
||||
] = useState<GQL.BulkPerformerUpdateInput>({});
|
||||
const [aggregateState, setAggregateState] =
|
||||
useState<GQL.BulkPerformerUpdateInput>({});
|
||||
// weight needs conversion to/from number
|
||||
const [weight, setWeight] = useState<string | undefined>();
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||
@@ -183,7 +181,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<Modal
|
||||
<ModalComponent
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
@@ -319,7 +317,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import {
|
||||
GridCard,
|
||||
CountryFlag,
|
||||
HoverPopover,
|
||||
Icon,
|
||||
TagLink,
|
||||
} from "src/components/Shared";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { GridCard } from "../Shared/GridCard";
|
||||
import { CountryFlag } from "../Shared/CountryFlag";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import {
|
||||
Criterion,
|
||||
@@ -19,6 +18,8 @@ import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
import GenderIcon from "./GenderIcon";
|
||||
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import cx from "classnames";
|
||||
import { usePerformerUpdate } from "src/core/StashService";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes: Criterion<CriterionValue>[];
|
||||
@@ -61,17 +62,39 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
{ age, years_old: ageL10String }
|
||||
);
|
||||
|
||||
function maybeRenderFavoriteIcon() {
|
||||
if (performer.favorite === false) {
|
||||
return;
|
||||
}
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
|
||||
function renderFavoriteIcon() {
|
||||
return (
|
||||
<div className="favorite">
|
||||
<Icon icon={faHeart} size="2x" />
|
||||
</div>
|
||||
<Link to="" onClick={(e) => e.preventDefault()}>
|
||||
<Button
|
||||
className={cx(
|
||||
"minimal",
|
||||
"mousetrap",
|
||||
"favorite-button",
|
||||
performer.favorite ? "favorite" : "not-favorite"
|
||||
)}
|
||||
onClick={() => onToggleFavorite!(!performer.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} size="2x" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function onToggleFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderScenesPopoverButton() {
|
||||
if (!performer.scene_count) return;
|
||||
|
||||
@@ -214,7 +237,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
{maybeRenderFavoriteIcon()}
|
||||
|
||||
{renderFavoriteIcon()}
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderFlag()}
|
||||
</>
|
||||
|
||||
@@ -12,17 +12,16 @@ import {
|
||||
usePerformerDestroy,
|
||||
mutateMetadataAutoTag,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
Counter,
|
||||
CountryFlag,
|
||||
DetailsEditNavbar,
|
||||
ErrorMessage,
|
||||
Icon,
|
||||
LoadingIndicator,
|
||||
} from "src/components/Shared";
|
||||
import { useLightbox, useToast } from "src/hooks";
|
||||
import { Counter } from "src/components/Shared/Counter";
|
||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { TextUtils } from "src/utils";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||
@@ -32,12 +31,8 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
||||
import GenderIcon from "../GenderIcon";
|
||||
import {
|
||||
faCamera,
|
||||
faDove,
|
||||
faHeart,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import { IUIConfig } from "src/core/config";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
|
||||
@@ -247,7 +242,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
isNew={false}
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
onCancelEditing={() => {
|
||||
@@ -351,7 +345,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faDove} />
|
||||
<Icon icon={faTwitter} />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
@@ -366,7 +360,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faCamera} />
|
||||
<Icon icon={faInstagram} />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
@@ -405,7 +399,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
<h2>
|
||||
<GenderIcon
|
||||
gender={performer.gender}
|
||||
className="gender-icon mr-2 flag-icon"
|
||||
className="gender-icon mr-2 fi"
|
||||
/>
|
||||
<CountryFlag country={performer.country} className="mr-2" />
|
||||
<span className="performer-name">{performer.name}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
@@ -8,13 +8,11 @@ const PerformerCreate: React.FC = () => {
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const performer = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const activeImage = imagePreview ?? "";
|
||||
const intl = useIntl();
|
||||
@@ -50,9 +48,8 @@ const PerformerCreate: React.FC = () => {
|
||||
/>
|
||||
</h2>
|
||||
<PerformerEditPanel
|
||||
performer={{ name: nameQuery ?? "" }}
|
||||
performer={performer}
|
||||
isVisible
|
||||
isNew
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user