diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eced257b4..4db7952e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches: [ develop, master ] pull_request: - branches: [ develop ] release: types: [ published ] diff --git a/README.md b/README.md index 1dff1bf58..ddfb8159a 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ Many community-maintained scrapers are available for download at the [Community # Translation [![Translate](https://translate.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/) -🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷 +🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷 -Stash is available in 15 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! +Stash is available in 16 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! # Support (FAQ) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 41ea82dcd..4119575a9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -15,10 +15,10 @@ NOTE: You may need to run the `go get` commands outside the project directory to ### Windows 1. Download and install [Go for Windows](https://golang.org/dl/) -2. Download and install [MingW](https://sourceforge.net/projects/mingw/) and select packages `mingw32-base` +2. Download and extract [MingW64](https://sourceforge.net/projects/mingw-w64/files/) (scroll down and select x86_64-posix-seh, dont use the autoinstaller it doesnt work) 3. Search for "advanced system settings" and open the system properties dialog. 1. Click the `Environment Variables` button - 2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace * with the correct path). + 2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace with the correct path to where you extracted MingW64). NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For example `make pre-ui` will be `mingw32-make pre-ui` diff --git a/go.mod b/go.mod index b225d3150..d6ccb07f6 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/kermieisinthehouse/gosx-notifier v0.1.1 github.com/kermieisinthehouse/systray v1.2.4 github.com/lucasb-eyer/go-colorful v1.2.0 + github.com/spf13/cast v1.4.1 github.com/vearutop/statigz v1.1.6 github.com/vektah/gqlparser/v2 v2.4.1 ) @@ -90,7 +91,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/zerolog v1.26.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cobra v1.4.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stretchr/objx v0.2.0 // indirect diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 9e7f853fe..58ba6d7a9 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -185,4 +185,5 @@ fragment ConfigData on ConfigResult { defaults { ...ConfigDefaultSettingsData } + ui } diff --git a/graphql/documents/mutations/config.graphql b/graphql/documents/mutations/config.graphql index fff7dbeca..dfd53ed75 100644 --- a/graphql/documents/mutations/config.graphql +++ b/graphql/documents/mutations/config.graphql @@ -36,6 +36,10 @@ mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) { } } +mutation ConfigureUI($input: Map!) { + configureUI(input: $input) +} + mutation GenerateAPIKey($input: GenerateAPIKeyInput!) { generateAPIKey(input: $input) } diff --git a/graphql/documents/queries/filter.graphql b/graphql/documents/queries/filter.graphql index 2c022fde7..67fbaf6cf 100644 --- a/graphql/documents/queries/filter.graphql +++ b/graphql/documents/queries/filter.graphql @@ -1,4 +1,10 @@ -query FindSavedFilters($mode: FilterMode!) { +query FindSavedFilter($id: ID!) { + findSavedFilter(id: $id) { + ...SavedFilterData + } +} + +query FindSavedFilters($mode: FilterMode) { findSavedFilters(mode: $mode) { ...SavedFilterData } diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 9b5bf6ed7..7229dce1d 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -1,7 +1,8 @@ """The query root for this schema""" type Query { # Filters - findSavedFilters(mode: FilterMode!): [SavedFilter!]! + findSavedFilter(id: ID!): SavedFilter + findSavedFilters(mode: FilterMode): [SavedFilter!]! findDefaultFilter(mode: FilterMode!): SavedFilter """Find a scene by ID or Checksum""" @@ -114,12 +115,6 @@ type Query { """Scrape a list of performers from a query""" scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones") - """Query StashBox for scenes""" - queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes") - """Query StashBox for performers""" - queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers") - # === end deprecated methods === - # Plugins """List loaded plugins""" plugins: [Plugin!] @@ -244,6 +239,11 @@ type Mutation { configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult! configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult! + # overwrites the entire UI configuration + configureUI(input: Map!): Map! + # sets a single UI key value + configureUISetting(key: String!, value: Any): Map! + """Generate and set (or clear) API key""" generateAPIKey(input: GenerateAPIKeyInput!): String! diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 281f133c4..9a84f0cc6 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -413,6 +413,7 @@ type ConfigResult { dlna: ConfigDLNAResult! scraping: ConfigScrapingResult! defaults: ConfigDefaultSettingsResult! + ui: Map! } """Directory structure of a path""" diff --git a/graphql/schema/types/scalars.graphql b/graphql/schema/types/scalars.graphql index 439f0d561..f973887a5 100644 --- a/graphql/schema/types/scalars.graphql +++ b/graphql/schema/types/scalars.graphql @@ -4,4 +4,9 @@ Timestamp is a point in time. It is always output as RFC3339-compatible time poi It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m" for "5 minutes in the future" """ -scalar Timestamp \ No newline at end of file +scalar Timestamp + +# generic JSON object +scalar Map + +scalar Any \ No newline at end of file diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 39bce5d3c..12f12d2a5 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -129,6 +129,12 @@ query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) { } } +query FindScenesBySceneFingerprints($fingerprints: [[FingerprintQueryInput!]!]!) { + findScenesBySceneFingerprints(fingerprints: $fingerprints) { + ...SceneFragment + } +} + query SearchScene($term: String!) { searchScene(term: $term) { ...SceneFragment diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 906378ca5..7413c413b 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -501,3 +501,23 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.Gene return newAPIKey, nil } + +func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { + c := config.GetInstance() + c.SetUIConfiguration(input) + + if err := c.Write(); err != nil { + return c.GetUIConfiguration(), err + } + + return c.GetUIConfiguration(), nil +} + +func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) { + c := config.GetInstance() + + cfg := c.GetUIConfiguration() + cfg[key] = value + + return r.ConfigureUI(ctx, cfg) +} diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index ad0a2c142..d0852ff13 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -66,6 +66,7 @@ func makeConfigResult() *models.ConfigResult { Dlna: makeConfigDLNAResult(), Scraping: makeConfigScrapingResult(), Defaults: makeConfigDefaultsResult(), + UI: makeConfigUIResult(), } } @@ -216,6 +217,10 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { } } +func makeConfigUIResult() map[string]interface{} { + return config.GetInstance().GetUIConfiguration() +} + func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input models.StashBoxInput) (*models.StashBoxValidationResult, error) { client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager) user, err := client.GetUser(ctx) diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index d79697701..a28ef2f59 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -2,13 +2,33 @@ package api import ( "context" + "strconv" "github.com/stashapp/stash/pkg/models" ) -func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.FilterMode) (ret []*models.SavedFilter, err error) { +func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) { + idInt, err := strconv.Atoi(id) + if err != nil { + return nil, err + } + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { - ret, err = repo.SavedFilter().FindByMode(mode) + ret, err = repo.SavedFilter().Find(idInt) + return err + }); err != nil { + return nil, err + } + return ret, err +} + +func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) { + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + if mode != nil { + ret, err = repo.SavedFilter().FindByMode(*mode) + } else { + ret, err = repo.SavedFilter().All() + } return err }); err != nil { return nil, err diff --git a/internal/api/resolver_query_scraper.go b/internal/api/resolver_query_scraper.go index 2208628d5..8fd6c345a 100644 --- a/internal/api/resolver_query_scraper.go +++ b/internal/api/resolver_query_scraper.go @@ -227,46 +227,6 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models return marshalScrapedMovie(content) } -func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxSceneQueryInput) ([]*models.ScrapedScene, error) { - boxes := config.GetInstance().GetStashBoxes() - - if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { - return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, input.StashBoxIndex) - } - - client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) - - if len(input.SceneIds) > 0 { - return client.FindStashBoxScenesByFingerprintsFlat(ctx, input.SceneIds) - } - - if input.Q != nil { - return client.QueryStashBoxScene(ctx, *input.Q) - } - - return nil, nil -} - -func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models.StashBoxPerformerQueryInput) ([]*models.StashBoxPerformerQueryResult, error) { - boxes := config.GetInstance().GetStashBoxes() - - if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { - return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, input.StashBoxIndex) - } - - client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) - - if len(input.PerformerIds) > 0 { - return client.FindStashBoxPerformersByNames(ctx, input.PerformerIds) - } - - if input.Q != nil { - return client.QueryStashBoxPerformer(ctx, *input.Q) - } - - return nil, nil -} - func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) { boxes := config.GetInstance().GetStashBoxes() @@ -280,6 +240,15 @@ func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) { func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) { var ret []*models.ScrapedScene + var sceneID int + if input.SceneID != nil { + var err error + sceneID, err = strconv.Atoi(*input.SceneID) + if err != nil { + return nil, fmt.Errorf("%w: sceneID is not an integer: '%s'", ErrInput, *input.SceneID) + } + } + switch { case source.ScraperID != nil: var err error @@ -288,11 +257,6 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.Scr switch { case input.SceneID != nil: - var sceneID int - sceneID, err = strconv.Atoi(*input.SceneID) - if err != nil { - return nil, fmt.Errorf("%w: sceneID is not an integer: '%s'", ErrInput, *input.SceneID) - } c, err = r.scraperCache().ScrapeID(ctx, *source.ScraperID, sceneID, models.ScrapeContentTypeScene) if c != nil { content = []models.ScrapedContent{c} @@ -324,7 +288,7 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.Scr switch { case input.SceneID != nil: - ret, err = client.FindStashBoxScenesByFingerprintsFlat(ctx, []string{*input.SceneID}) + ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID) case input.Query != nil: ret, err = client.QueryStashBoxScene(ctx, *input.Query) default: @@ -352,7 +316,12 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source models.Scr return nil, err } - return client.FindStashBoxScenesByFingerprints(ctx, input.SceneIds) + sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds) + if err != nil { + return nil, err + } + + return client.FindStashBoxScenesByFingerprints(ctx, sceneIDs) } return nil, errors.New("scraper_id or stash_box_index must be set") diff --git a/internal/api/server.go b/internal/api/server.go index a8e2f25dc..c89e20d48 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -227,7 +227,7 @@ func Start() error { prefix := getProxyPrefix(r.Header) if prefix != "" { - r.URL.Path = strings.Replace(r.URL.Path, prefix, "", 1) + r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) } r.URL.Path = uiRootDir + r.URL.Path diff --git a/internal/autotag/gallery.go b/internal/autotag/gallery.go index 603e3e36a..3bdfd3c15 100644 --- a/internal/autotag/gallery.go +++ b/internal/autotag/gallery.go @@ -7,12 +7,16 @@ import ( ) func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger { + // only trim the extension if gallery is file-based + trimExt := s.Zip + return tagger{ - ID: s.ID, - Type: "gallery", - Name: s.GetTitle(), - Path: s.Path.String, - cache: cache, + ID: s.ID, + Type: "gallery", + Name: s.GetTitle(), + Path: s.Path.String, + trimExt: trimExt, + cache: cache, } } diff --git a/internal/autotag/scene_test.go b/internal/autotag/scene_test.go index 190b16b8e..578b9e7f6 100644 --- a/internal/autotag/scene_test.go +++ b/internal/autotag/scene_test.go @@ -34,6 +34,7 @@ func generateNamePatterns(name, separator, ext string) []string { ret = append(ret, fmt.Sprintf("aaa%s%s.%s", separator, name, ext)) ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.%s", separator, name, separator, ext)) ret = append(ret, fmt.Sprintf("dir/%s%saaa.%s", name, separator, ext)) + ret = append(ret, fmt.Sprintf("dir%sdir/%s%saaa.%s", separator, name, separator, ext)) ret = append(ret, fmt.Sprintf("dir\\%s%saaa.%s", name, separator, ext)) ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.%s", name, separator, ext)) ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.%s", name, separator, ext)) diff --git a/internal/autotag/tagger.go b/internal/autotag/tagger.go index 624d29f5a..4ea1fbc01 100644 --- a/internal/autotag/tagger.go +++ b/internal/autotag/tagger.go @@ -22,10 +22,11 @@ import ( ) type tagger struct { - ID int - Type string - Name string - Path string + ID int + Type string + Name string + Path string + trimExt bool cache *match.Cache } @@ -41,7 +42,7 @@ func (t *tagger) addLog(otherType, otherName string) { } func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc addLinkFunc) error { - others, err := match.PathToPerformers(t.Path, performerReader, t.cache) + others, err := match.PathToPerformers(t.Path, performerReader, t.cache, t.trimExt) if err != nil { return err } @@ -62,7 +63,7 @@ func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc a } func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFunc) error { - studio, err := match.PathToStudio(t.Path, studioReader, t.cache) + studio, err := match.PathToStudio(t.Path, studioReader, t.cache, t.trimExt) if err != nil { return err } @@ -83,7 +84,7 @@ func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFun } func (t *tagger) tagTags(tagReader models.TagReader, addFunc addLinkFunc) error { - others, err := match.PathToTags(t.Path, tagReader, t.cache) + others, err := match.PathToTags(t.Path, tagReader, t.cache, t.trimExt) if err != nil { return err } diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 1dbfa1a62..71847608e 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -153,6 +153,8 @@ const ( ImageLightboxScrollMode = "image_lightbox.scroll_mode" ImageLightboxScrollAttemptsBeforeChange = "image_lightbox.scroll_attempts_before_change" + UI = "ui" + defaultImageLightboxSlideshowDelay = 5000 DisableDropdownCreatePerformer = "disable_dropdown_create.performer" @@ -971,6 +973,26 @@ func (i *Instance) GetDisableDropdownCreate() *models.ConfigDisableDropdownCreat } } +func (i *Instance) GetUIConfiguration() map[string]interface{} { + i.RLock() + defer i.RUnlock() + + // HACK: viper changes map keys to case insensitive values, so the workaround is to + // convert map keys to snake case for storage + v := i.viper(UI).GetStringMap(UI) + + return fromSnakeCaseMap(v) +} + +func (i *Instance) SetUIConfiguration(v map[string]interface{}) { + i.RLock() + defer i.RUnlock() + + // HACK: viper changes map keys to case insensitive values, so the workaround is to + // convert map keys to snake case for storage + i.viper(UI).Set(UI, toSnakeCaseMap(v)) +} + func (i *Instance) GetCSSPath() string { // use custom.css in the same directory as the config file configFileUsed := i.GetConfigFile() diff --git a/internal/manager/config/map.go b/internal/manager/config/map.go new file mode 100644 index 000000000..c75003adc --- /dev/null +++ b/internal/manager/config/map.go @@ -0,0 +1,100 @@ +package config + +import ( + "bytes" + "unicode" + + "github.com/spf13/cast" +) + +// HACK: viper changes map keys to case insensitive values, so the workaround is to +// convert the map to use snake-case keys + +// toSnakeCase converts a string to snake_case +// NOTE: a double capital will be converted in a way that will yield a different result +// when converted back to camel case. +// For example: someIDs => some_ids => someIds +func toSnakeCase(v string) string { + var buf bytes.Buffer + underscored := false + for i, c := range v { + if !underscored && unicode.IsUpper(c) && i > 0 { + buf.WriteByte('_') + underscored = true + } else { + underscored = false + } + + buf.WriteRune(unicode.ToLower(c)) + } + return buf.String() +} + +func fromSnakeCase(v string) string { + var buf bytes.Buffer + cap := false + for i, c := range v { + switch { + case c == '_' && i > 0: + cap = true + case cap: + buf.WriteRune(unicode.ToUpper(c)) + cap = false + default: + buf.WriteRune(c) + } + } + return buf.String() +} + +// copyAndInsensitiviseMap behaves like insensitiviseMap, but creates a copy of +// any map it makes case insensitive. +func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} { + nm := make(map[string]interface{}) + + for key, val := range m { + adjKey := toSnakeCase(key) + nm[adjKey] = val + } + + return nm +} + +// convertMapValue converts values into something that can be marshalled in JSON +// This means converting map[interface{}]interface{} to map[string]interface{} where ever +// encountered. +func convertMapValue(val interface{}) interface{} { + switch v := val.(type) { + case map[interface{}]interface{}: + ret := cast.ToStringMap(v) + for k, vv := range ret { + ret[k] = convertMapValue(vv) + } + return ret + case map[string]interface{}: + ret := make(map[string]interface{}) + for k, vv := range v { + ret[k] = convertMapValue(vv) + } + return ret + case []interface{}: + ret := make([]interface{}, len(v)) + for i, vv := range v { + ret[i] = convertMapValue(vv) + } + return ret + default: + return v + } +} + +func fromSnakeCaseMap(m map[string]interface{}) map[string]interface{} { + nm := make(map[string]interface{}) + + for key, val := range m { + adjKey := fromSnakeCase(key) + nm[adjKey] = convertMapValue(val) + } + + return nm +} diff --git a/internal/manager/config/map_test.go b/internal/manager/config/map_test.go new file mode 100644 index 000000000..3c7da15b2 --- /dev/null +++ b/internal/manager/config/map_test.go @@ -0,0 +1,82 @@ +package config + +import ( + "testing" +) + +func Test_toSnakeCase(t *testing.T) { + tests := []struct { + name string + v string + want string + }{ + { + "basic", + "basic", + "basic", + }, + { + "two words", + "twoWords", + "two_words", + }, + { + "three word value", + "threeWordValue", + "three_word_value", + }, + { + "snake case", + "snake_case", + "snake_case", + }, + { + "double capital", + "doubleCApital", + "double_capital", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toSnakeCase(tt.v); got != tt.want { + t.Errorf("toSnakeCase() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fromSnakeCase(t *testing.T) { + tests := []struct { + name string + v string + want string + }{ + { + "basic", + "basic", + "basic", + }, + { + "two words", + "two_words", + "twoWords", + }, + { + "three word value", + "three_word_value", + "threeWordValue", + }, + { + "camel case", + "camelCase", + "camelCase", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fromSnakeCase(tt.v); got != tt.want { + t.Errorf("fromSnakeCase() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/manager/task_identify.go b/internal/manager/task_identify.go index 54bb063e4..678d0c7b3 100644 --- a/internal/manager/task_identify.go +++ b/internal/manager/task_identify.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/stashapp/stash/internal/identify" "github.com/stashapp/stash/pkg/job" @@ -212,7 +211,7 @@ type stashboxSource struct { } func (s stashboxSource) ScrapeScene(ctx context.Context, sceneID int) (*models.ScrapedScene, error) { - results, err := s.FindStashBoxScenesByFingerprintsFlat(ctx, []string{strconv.Itoa(sceneID)}) + results, err := s.FindStashBoxSceneByFingerprints(ctx, sceneID) if err != nil { return nil, fmt.Errorf("error querying stash-box using scene ID %d: %w", sceneID, err) } diff --git a/pkg/exec/shell_windows.go b/pkg/exec/shell_windows.go index 9f4b551c0..84b7ea206 100644 --- a/pkg/exec/shell_windows.go +++ b/pkg/exec/shell_windows.go @@ -12,5 +12,5 @@ import ( // hideExecShell hides the windows when executing on Windows. func hideExecShell(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.DETACHED_PROCESS} + cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.DETACHED_PROCESS & windows.CREATE_NO_WINDOW} } diff --git a/pkg/gallery/scan.go b/pkg/gallery/scan.go index 3528c8b93..f45a26d77 100644 --- a/pkg/gallery/scan.go +++ b/pkg/gallery/scan.go @@ -187,7 +187,8 @@ func (scanner *Scanner) ScanNew(ctx context.Context, file file.SourceFile) (retG scanner.PluginCache.ExecutePostHooks(ctx, g.ID, plugin.GalleryUpdatePost, nil, nil) } - scanImages = isNewGallery + // Also scan images if zip file has been moved (ie updated) as the image paths are no longer valid + scanImages = isNewGallery || isUpdatedGallery retGallery = g return diff --git a/pkg/match/path.go b/pkg/match/path.go index 47a7ad26e..4f20423dd 100644 --- a/pkg/match/path.go +++ b/pkg/match/path.go @@ -37,13 +37,15 @@ func getPathQueryRegex(name string) string { return ret } -func getPathWords(path string) []string { +func getPathWords(path string, trimExt bool) []string { retStr := path - // remove the extension - ext := filepath.Ext(retStr) - if ext != "" { - retStr = strings.TrimSuffix(retStr, ext) + if trimExt { + // remove the extension + ext := filepath.Ext(retStr) + if ext != "" { + retStr = strings.TrimSuffix(retStr, ext) + } } // handle path separators @@ -136,8 +138,8 @@ func getPerformers(words []string, performerReader models.PerformerReader, cache return append(performers, swPerformers...), nil } -func PathToPerformers(path string, reader models.PerformerReader, cache *Cache) ([]*models.Performer, error) { - words := getPathWords(path) +func PathToPerformers(path string, reader models.PerformerReader, cache *Cache, trimExt bool) ([]*models.Performer, error) { + words := getPathWords(path, trimExt) performers, err := getPerformers(words, reader, cache) if err != nil { @@ -172,8 +174,8 @@ func getStudios(words []string, reader models.StudioReader, cache *Cache) ([]*mo // PathToStudio returns the Studio that matches the given path. // Where multiple matching studios are found, the one that matches the latest // position in the path is returned. -func PathToStudio(path string, reader models.StudioReader, cache *Cache) (*models.Studio, error) { - words := getPathWords(path) +func PathToStudio(path string, reader models.StudioReader, cache *Cache, trimExt bool) (*models.Studio, error) { + words := getPathWords(path, trimExt) candidates, err := getStudios(words, reader, cache) if err != nil { @@ -220,8 +222,8 @@ func getTags(words []string, reader models.TagReader, cache *Cache) ([]*models.T return append(tags, swTags...), nil } -func PathToTags(path string, reader models.TagReader, cache *Cache) ([]*models.Tag, error) { - words := getPathWords(path) +func PathToTags(path string, reader models.TagReader, cache *Cache, trimExt bool) ([]*models.Tag, error) { + words := getPathWords(path, trimExt) tags, err := getTags(words, reader, cache) if err != nil { diff --git a/pkg/models/jsonschema/movie.go b/pkg/models/jsonschema/movie.go index 4c33da38f..d4eded802 100644 --- a/pkg/models/jsonschema/movie.go +++ b/pkg/models/jsonschema/movie.go @@ -5,6 +5,8 @@ import ( "os" jsoniter "github.com/json-iterator/go" + + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models/json" ) @@ -15,7 +17,7 @@ type Movie struct { Date string `json:"date,omitempty"` Rating int `json:"rating,omitempty"` Director string `json:"director,omitempty"` - Synopsis string `json:"sypnopsis,omitempty"` + Synopsis string `json:"synopsis,omitempty"` FrontImage string `json:"front_image,omitempty"` BackImage string `json:"back_image,omitempty"` URL string `json:"url,omitempty"` @@ -24,6 +26,11 @@ type Movie struct { UpdatedAt json.JSONTime `json:"updated_at,omitempty"` } +// Backwards Compatible synopsis for the movie +type MovieSynopsisBC struct { + Synopsis string `json:"sypnopsis,omitempty"` +} + func LoadMovieFile(filePath string) (*Movie, error) { var movie Movie file, err := os.Open(filePath) @@ -37,6 +44,22 @@ func LoadMovieFile(filePath string) (*Movie, error) { if err != nil { return nil, err } + if movie.Synopsis == "" { + // keep backwards compatibility with pre #2664 builds + // attempt to get the synopsis from the alternate (sypnopsis) key + + _, err = file.Seek(0, 0) // seek to start of file + if err == nil { + var synopsis MovieSynopsisBC + err = jsonParser.Decode(&synopsis) + if err == nil { + movie.Synopsis = synopsis.Synopsis + if movie.Synopsis != "" { + logger.Debug("Movie synopsis retrieved from alternate key") + } + } + } + } return &movie, nil } diff --git a/pkg/models/mocks/SavedFilterReaderWriter.go b/pkg/models/mocks/SavedFilterReaderWriter.go index 987fdd5fc..952497be2 100644 --- a/pkg/models/mocks/SavedFilterReaderWriter.go +++ b/pkg/models/mocks/SavedFilterReaderWriter.go @@ -12,6 +12,29 @@ type SavedFilterReaderWriter struct { mock.Mock } +// All provides a mock function with given fields: +func (_m *SavedFilterReaderWriter) All() ([]*models.SavedFilter, error) { + ret := _m.Called() + + var r0 []*models.SavedFilter + if rf, ok := ret.Get(0).(func() []*models.SavedFilter); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SavedFilter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Create provides a mock function with given fields: obj func (_m *SavedFilterReaderWriter) Create(obj models.SavedFilter) (*models.SavedFilter, error) { ret := _m.Called(obj) @@ -118,6 +141,29 @@ func (_m *SavedFilterReaderWriter) FindDefault(mode models.FilterMode) (*models. return r0, r1 } +// FindMany provides a mock function with given fields: ids, ignoreNotFound +func (_m *SavedFilterReaderWriter) FindMany(ids []int, ignoreNotFound bool) ([]*models.SavedFilter, error) { + ret := _m.Called(ids, ignoreNotFound) + + var r0 []*models.SavedFilter + if rf, ok := ret.Get(0).(func([]int, bool) []*models.SavedFilter); ok { + r0 = rf(ids, ignoreNotFound) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SavedFilter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]int, bool) error); ok { + r1 = rf(ids, ignoreNotFound) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SetDefault provides a mock function with given fields: obj func (_m *SavedFilterReaderWriter) SetDefault(obj models.SavedFilter) (*models.SavedFilter, error) { ret := _m.Called(obj) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index a200087ea..0635fd200 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -482,6 +482,7 @@ func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { return r0, r1 } +// GetCaptions provides a mock function with given fields: sceneID func (_m *SceneReaderWriter) GetCaptions(sceneID int) ([]*models.SceneCaption, error) { ret := _m.Called(sceneID) @@ -751,13 +752,13 @@ func (_m *SceneReaderWriter) Update(updatedScene models.ScenePartial) (*models.S return r0, r1 } -// UpdateCaptions provides a mock function with given fields: id, newCaptions -func (_m *SceneReaderWriter) UpdateCaptions(sceneID int, captions []*models.SceneCaption) error { - ret := _m.Called(sceneID, captions) +// UpdateCaptions provides a mock function with given fields: id, captions +func (_m *SceneReaderWriter) UpdateCaptions(id int, captions []*models.SceneCaption) error { + ret := _m.Called(id, captions) var r0 error if rf, ok := ret.Get(0).(func(int, []*models.SceneCaption) error); ok { - r0 = rf(sceneID, captions) + r0 = rf(id, captions) } else { r0 = ret.Error(0) } diff --git a/pkg/models/saved_filter.go b/pkg/models/saved_filter.go index e455d92c4..e6cd2f8e0 100644 --- a/pkg/models/saved_filter.go +++ b/pkg/models/saved_filter.go @@ -1,7 +1,9 @@ package models type SavedFilterReader interface { + All() ([]*SavedFilter, error) Find(id int) (*SavedFilter, error) + FindMany(ids []int, ignoreNotFound bool) ([]*SavedFilter, error) FindByMode(mode FilterMode) ([]*SavedFilter, error) FindDefault(mode FilterMode) (*SavedFilter, error) } diff --git a/pkg/scraper/autotag.go b/pkg/scraper/autotag.go index 20940fce2..4a86d8df2 100644 --- a/pkg/scraper/autotag.go +++ b/pkg/scraper/autotag.go @@ -21,8 +21,8 @@ type autotagScraper struct { globalConfig GlobalConfig } -func autotagMatchPerformers(path string, performerReader models.PerformerReader) ([]*models.ScrapedPerformer, error) { - p, err := match.PathToPerformers(path, performerReader, nil) +func autotagMatchPerformers(path string, performerReader models.PerformerReader, trimExt bool) ([]*models.ScrapedPerformer, error) { + p, err := match.PathToPerformers(path, performerReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching performers: %w", err) } @@ -45,8 +45,8 @@ func autotagMatchPerformers(path string, performerReader models.PerformerReader) return ret, nil } -func autotagMatchStudio(path string, studioReader models.StudioReader) (*models.ScrapedStudio, error) { - studio, err := match.PathToStudio(path, studioReader, nil) +func autotagMatchStudio(path string, studioReader models.StudioReader, trimExt bool) (*models.ScrapedStudio, error) { + studio, err := match.PathToStudio(path, studioReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching studios: %w", err) } @@ -62,8 +62,8 @@ func autotagMatchStudio(path string, studioReader models.StudioReader) (*models. return nil, nil } -func autotagMatchTags(path string, tagReader models.TagReader) ([]*models.ScrapedTag, error) { - t, err := match.PathToTags(path, tagReader, nil) +func autotagMatchTags(path string, tagReader models.TagReader, trimExt bool) ([]*models.ScrapedTag, error) { + t, err := match.PathToTags(path, tagReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching tags: %w", err) } @@ -85,20 +85,21 @@ func autotagMatchTags(path string, tagReader models.TagReader) ([]*models.Scrape func (s autotagScraper) viaScene(ctx context.Context, _client *http.Client, scene *models.Scene) (*models.ScrapedScene, error) { var ret *models.ScrapedScene + const trimExt = false // populate performers, studio and tags based on scene path if err := s.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { path := scene.Path - performers, err := autotagMatchPerformers(path, r.Performer()) + performers, err := autotagMatchPerformers(path, r.Performer(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } - studio, err := autotagMatchStudio(path, r.Studio()) + studio, err := autotagMatchStudio(path, r.Studio(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } - tags, err := autotagMatchTags(path, r.Tag()) + tags, err := autotagMatchTags(path, r.Tag(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } @@ -125,21 +126,24 @@ func (s autotagScraper) viaGallery(ctx context.Context, _client *http.Client, ga return nil, nil } + // only trim extension if gallery is file-based + trimExt := gallery.Zip + var ret *models.ScrapedGallery // populate performers, studio and tags based on scene path if err := s.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { path := gallery.Path.String - performers, err := autotagMatchPerformers(path, r.Performer()) + performers, err := autotagMatchPerformers(path, r.Performer(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } - studio, err := autotagMatchStudio(path, r.Studio()) + studio, err := autotagMatchStudio(path, r.Studio(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } - tags, err := autotagMatchTags(path, r.Tag()) + tags, err := autotagMatchTags(path, r.Tag(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index 37985a20f..a41380740 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -12,6 +12,7 @@ import ( type StashBoxGraphQLClient interface { FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) + FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) @@ -31,32 +32,33 @@ func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOp } type Query struct { - FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" - QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" - FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" - QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" - FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" - QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" - FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" - QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" - FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" - FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" - FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" - FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" - QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" - FindSite *Site "json:\"findSite\" graphql:\"findSite\"" - QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" - FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" - QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" - FindUser *User "json:\"findUser\" graphql:\"findUser\"" - QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" - Me *User "json:\"me\" graphql:\"me\"" - SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" - SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" - FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" - FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" - Version Version "json:\"version\" graphql:\"version\"" - GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" + FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" + QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" + FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" + QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" + FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" + QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" + FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" + QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" + FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" + FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" + FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" + FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" + FindScenesBySceneFingerprints [][]*Scene "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" + QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" + FindSite *Site "json:\"findSite\" graphql:\"findSite\"" + QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" + FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" + QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" + FindUser *User "json:\"findUser\" graphql:\"findUser\"" + QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" + Me *User "json:\"me\" graphql:\"me\"" + SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" + SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" + FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" + FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" + Version Version "json:\"version\" graphql:\"version\"" + GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" } type Mutation struct { SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" @@ -95,6 +97,10 @@ type Mutation struct { PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" + SceneEditUpdate Edit "json:\"sceneEditUpdate\" graphql:\"sceneEditUpdate\"" + PerformerEditUpdate Edit "json:\"performerEditUpdate\" graphql:\"performerEditUpdate\"" + StudioEditUpdate Edit "json:\"studioEditUpdate\" graphql:\"studioEditUpdate\"" + TagEditUpdate Edit "json:\"tagEditUpdate\" graphql:\"tagEditUpdate\"" EditVote Edit "json:\"editVote\" graphql:\"editVote\"" EditComment Edit "json:\"editComment\" graphql:\"editComment\"" ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" @@ -190,6 +196,9 @@ type FindSceneByFingerprint struct { type FindScenesByFullFingerprints struct { FindScenesByFullFingerprints []*SceneFragment "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" } +type FindScenesBySceneFingerprints struct { + FindScenesBySceneFingerprints [][]*SceneFragment "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" +} type SearchScene struct { SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" } @@ -230,6 +239,52 @@ fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} fragment StudioFragment on Studio { name id @@ -240,6 +295,12 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment PerformerFragment on Performer { id name @@ -278,62 +339,10 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} fragment BodyModificationFragment on BodyModification { location description } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} ` func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { @@ -354,6 +363,31 @@ const FindScenesByFullFingerprintsDocument = `query FindScenesByFullFingerprints ... SceneFragment } } +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} fragment URLFragment on URL { url type @@ -374,6 +408,25 @@ fragment PerformerAppearanceFragment on PerformerAppearance { ... PerformerFragment } } +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} fragment PerformerFragment on Performer { id name @@ -412,56 +465,12 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment TagFragment on Tag { - name - id -} fragment MeasurementsFragment on Measurements { band_size cup_size waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment ImageFragment on Image { - id - url - width - height -} ` func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { @@ -477,8 +486,8 @@ func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints return &res, nil } -const SearchSceneDocument = `query SearchScene ($term: String!) { - searchScene(term: $term) { +const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprints ($fingerprints: [[FingerprintQueryInput!]!]!) { + findScenesBySceneFingerprints(fingerprints: $fingerprints) { ... SceneFragment } } @@ -486,54 +495,6 @@ fragment URLFragment on URL { url type } -fragment TagFragment on Tag { - name - id -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} fragment ImageFragment on Image { id url @@ -550,12 +511,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment PerformerFragment on Performer { id name @@ -590,6 +545,188 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment BodyModificationFragment on BodyModification { + location + description +} +` + +func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) { + vars := map[string]interface{}{ + "fingerprints": fingerprints, + } + + var res FindScenesBySceneFingerprints + if err := c.Client.Post(ctx, "FindScenesBySceneFingerprints", FindScenesBySceneFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { + return nil, err + } + + return &res, nil +} + +const SearchSceneDocument = `query SearchScene ($term: String!) { + searchScene(term: $term) { + ... SceneFragment + } +} +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + merged_ids + urls { + ... URLFragment + } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color + height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { @@ -610,30 +747,6 @@ const SearchPerformerDocument = `query SearchPerformer ($term: String!) { ... PerformerFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -668,6 +781,30 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { @@ -688,10 +825,6 @@ const FindPerformerByIDDocument = `query FindPerformerByID ($id: ID!) { ... PerformerFragment } } -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -746,6 +879,10 @@ fragment MeasurementsFragment on Measurements { waist hip } +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) { @@ -766,6 +903,24 @@ const FindSceneByIDDocument = `query FindSceneByID ($id: ID!) { ... SceneFragment } } +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} fragment PerformerFragment on Performer { id name @@ -800,49 +955,16 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} fragment MeasurementsFragment on Measurements { band_size cup_size waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment URLFragment on URL { - url - type -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment SceneFragment on Scene { id title @@ -868,11 +990,26 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment ImageFragment on Image { +fragment StudioFragment on Studio { + name id - url - width - height + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } ` diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 2ce0301bb..341f91d14 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -130,7 +130,9 @@ type Edit struct { Status VoteStatusEnum `json:"status"` Applied bool `json:"applied"` Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Updated *time.Time `json:"updated,omitempty"` + Closed *time.Time `json:"closed,omitempty"` + Expires *time.Time `json:"expires,omitempty"` } type EditComment struct { @@ -149,8 +151,6 @@ type EditInput struct { // Not required for create type ID *string `json:"id,omitempty"` Operation OperationEnum `json:"operation"` - // Required for amending an existing edit - EditID *string `json:"edit_id,omitempty"` // Only required for merge type MergeSourceIds []string `json:"merge_source_ids,omitempty"` Comment *string `json:"comment,omitempty"` @@ -206,15 +206,13 @@ type Fingerprint struct { } type FingerprintEditInput struct { - UserIds []string `json:"user_ids,omitempty"` - Hash string `json:"hash"` - Algorithm FingerprintAlgorithm `json:"algorithm"` - Duration int `json:"duration"` - Created time.Time `json:"created"` - // @deprecated(reason: "unused") - Submissions *int `json:"submissions,omitempty"` - // @deprecated(reason: "unused") - Updated *time.Time `json:"updated,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + Hash string `json:"hash"` + Algorithm FingerprintAlgorithm `json:"algorithm"` + Duration int `json:"duration"` + Created time.Time `json:"created"` + Submissions *int `json:"submissions,omitempty"` + Updated *time.Time `json:"updated,omitempty"` } type FingerprintInput struct { @@ -241,11 +239,6 @@ type FuzzyDate struct { Accuracy DateAccuracyEnum `json:"accuracy"` } -type FuzzyDateInput struct { - Date string `json:"date"` - Accuracy DateAccuracyEnum `json:"accuracy"` -} - type GrantInviteInput struct { UserID string `json:"user_id"` Amount int `json:"amount"` @@ -294,13 +287,6 @@ type Measurements struct { Hip *int `json:"hip,omitempty"` } -type MeasurementsInput struct { - CupSize *string `json:"cup_size,omitempty"` - BandSize *int `json:"band_size,omitempty"` - Waist *int `json:"waist,omitempty"` - Hip *int `json:"hip,omitempty"` -} - type MultiIDCriterionInput struct { Value []string `json:"value,omitempty"` Modifier CriterionModifier `json:"modifier"` @@ -324,6 +310,7 @@ type Performer struct { Gender *GenderEnum `json:"gender,omitempty"` Urls []*URL `json:"urls,omitempty"` Birthdate *FuzzyDate `json:"birthdate,omitempty"` + BirthDate *string `json:"birth_date,omitempty"` Age *int `json:"age,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` @@ -332,6 +319,10 @@ type Performer struct { // Height in cm Height *int `json:"height,omitempty"` Measurements *Measurements `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -369,13 +360,16 @@ type PerformerCreateInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -390,6 +384,7 @@ type PerformerDestroyInput struct { } type PerformerDraft struct { + ID *string `json:"id,omitempty"` Name string `json:"name"` Aliases *string `json:"aliases,omitempty"` Gender *string `json:"gender,omitempty"` @@ -412,6 +407,7 @@ type PerformerDraft struct { func (PerformerDraft) IsDraftData() {} type PerformerDraftInput struct { + ID *string `json:"id,omitempty"` Name string `json:"name"` Aliases *string `json:"aliases,omitempty"` Gender *string `json:"gender,omitempty"` @@ -432,19 +428,18 @@ type PerformerDraftInput struct { } type PerformerEdit struct { - Name *string `json:"name,omitempty"` - Disambiguation *string `json:"disambiguation,omitempty"` - AddedAliases []string `json:"added_aliases,omitempty"` - RemovedAliases []string `json:"removed_aliases,omitempty"` - Gender *GenderEnum `json:"gender,omitempty"` - AddedUrls []*URL `json:"added_urls,omitempty"` - RemovedUrls []*URL `json:"removed_urls,omitempty"` - Birthdate *string `json:"birthdate,omitempty"` - BirthdateAccuracy *string `json:"birthdate_accuracy,omitempty"` - Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` - Country *string `json:"country,omitempty"` - EyeColor *EyeColorEnum `json:"eye_color,omitempty"` - HairColor *HairColorEnum `json:"hair_color,omitempty"` + Name *string `json:"name,omitempty"` + Disambiguation *string `json:"disambiguation,omitempty"` + AddedAliases []string `json:"added_aliases,omitempty"` + RemovedAliases []string `json:"removed_aliases,omitempty"` + Gender *GenderEnum `json:"gender,omitempty"` + AddedUrls []*URL `json:"added_urls,omitempty"` + RemovedUrls []*URL `json:"removed_urls,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` + Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` + Country *string `json:"country,omitempty"` + EyeColor *EyeColorEnum `json:"eye_color,omitempty"` + HairColor *HairColorEnum `json:"hair_color,omitempty"` // Height in cm Height *int `json:"height,omitempty"` CupSize *string `json:"cup_size,omitempty"` @@ -461,6 +456,11 @@ type PerformerEdit struct { AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` DraftID *string `json:"draft_id,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Urls []*URL `json:"urls,omitempty"` + Images []*Image `json:"images,omitempty"` + Tattoos []*BodyModification `json:"tattoos,omitempty"` + Piercings []*BodyModification `json:"piercings,omitempty"` } func (PerformerEdit) IsEditDetails() {} @@ -471,13 +471,16 @@ type PerformerEditDetailsInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -510,7 +513,7 @@ type PerformerEditOptionsInput struct { } type PerformerQueryInput struct { - // Searches name and aliases - assumes like query unless quoted + // Searches name and disambiguation - assumes like query unless quoted Names *string `json:"names,omitempty"` // Searches name only - assumes like query unless quoted Name *string `json:"name,omitempty"` @@ -557,13 +560,16 @@ type PerformerUpdateInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -631,6 +637,7 @@ type Scene struct { Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` Date *string `json:"date,omitempty"` + ReleaseDate *string `json:"release_date,omitempty"` Urls []*URL `json:"urls,omitempty"` Studio *Studio `json:"studio,omitempty"` Tags []*Tag `json:"tags,omitempty"` @@ -652,7 +659,7 @@ type SceneCreateInput struct { Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Date *string `json:"date,omitempty"` + Date string `json:"date"` StudioID *string `json:"studio_id,omitempty"` Performers []*PerformerAppearanceInput `json:"performers,omitempty"` TagIds []string `json:"tag_ids,omitempty"` @@ -668,6 +675,7 @@ type SceneDestroyInput struct { } type SceneDraft struct { + ID *string `json:"id,omitempty"` Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` URL *URL `json:"url,omitempty"` @@ -701,6 +709,11 @@ type SceneEdit struct { Director *string `json:"director,omitempty"` Code *string `json:"code,omitempty"` DraftID *string `json:"draft_id,omitempty"` + Urls []*URL `json:"urls,omitempty"` + Performers []*PerformerAppearance `json:"performers,omitempty"` + Tags []*Tag `json:"tags,omitempty"` + Images []*Image `json:"images,omitempty"` + Fingerprints []*Fingerprint `json:"fingerprints,omitempty"` } func (SceneEdit) IsEditDetails() {} @@ -833,8 +846,8 @@ type Studio struct { Updated time.Time `json:"updated"` } -func (Studio) IsSceneDraftStudio() {} func (Studio) IsEditTarget() {} +func (Studio) IsSceneDraftStudio() {} type StudioCreateInput struct { Name string `json:"name"` @@ -855,6 +868,8 @@ type StudioEdit struct { Parent *Studio `json:"parent,omitempty"` AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` + Images []*Image `json:"images,omitempty"` + Urls []*URL `json:"urls,omitempty"` } func (StudioEdit) IsEditDetails() {} @@ -909,8 +924,8 @@ type Tag struct { Updated time.Time `json:"updated"` } -func (Tag) IsSceneDraftTag() {} func (Tag) IsEditTarget() {} +func (Tag) IsSceneDraftTag() {} type TagCategory struct { ID string `json:"id"` @@ -953,6 +968,7 @@ type TagEdit struct { AddedAliases []string `json:"added_aliases,omitempty"` RemovedAliases []string `json:"removed_aliases,omitempty"` Category *TagCategory `json:"category,omitempty"` + Aliases []string `json:"aliases,omitempty"` } func (TagEdit) IsEditDetails() {} @@ -1256,16 +1272,18 @@ type EditSortEnum string const ( EditSortEnumCreatedAt EditSortEnum = "CREATED_AT" EditSortEnumUpdatedAt EditSortEnum = "UPDATED_AT" + EditSortEnumClosedAt EditSortEnum = "CLOSED_AT" ) var AllEditSortEnum = []EditSortEnum{ EditSortEnumCreatedAt, EditSortEnumUpdatedAt, + EditSortEnumClosedAt, } func (e EditSortEnum) IsValid() bool { switch e { - case EditSortEnumCreatedAt, EditSortEnumUpdatedAt: + case EditSortEnumCreatedAt, EditSortEnumUpdatedAt, EditSortEnumClosedAt: return true } return false diff --git a/pkg/scraper/stashbox/graphql/override.go b/pkg/scraper/stashbox/graphql/override.go index 492a55a06..d80b74307 100644 --- a/pkg/scraper/stashbox/graphql/override.go +++ b/pkg/scraper/stashbox/graphql/override.go @@ -5,6 +5,7 @@ import "github.com/99designs/gqlgen/graphql" // Override for generated struct due to mistaken omitempty // https://github.com/Yamashou/gqlgenc/issues/77 type SceneDraftInput struct { + ID *string `json:"id,omitempty"` Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` URL *string `json:"url,omitempty"` diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 8b0af9f55..8470505d8 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/Yamashou/gqlgenc/client" - "github.com/corona10/goimagehash" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -24,7 +23,6 @@ import ( "github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scraper/stashbox/graphql" - "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" ) @@ -78,127 +76,21 @@ func (c Client) QueryStashBoxScene(ctx context.Context, queryStr string) ([]*mod return ret, nil } -func phashMatches(hash, other int64) bool { - // HACK - stash-box match distance is configurable. This needs to be fixed on - // the stash-box end. - const stashBoxDistance = 4 - - imageHash := goimagehash.NewImageHash(uint64(hash), goimagehash.PHash) - otherHash := goimagehash.NewImageHash(uint64(other), goimagehash.PHash) - - distance, _ := imageHash.Distance(otherHash) - return distance <= stashBoxDistance +// FindStashBoxScenesByFingerprints queries stash-box for a scene using the +// scene's MD5/OSHASH checksum, or PHash. +func (c Client) FindStashBoxSceneByFingerprints(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error) { + res, err := c.FindStashBoxScenesByFingerprints(ctx, []int{sceneID}) + if len(res) > 0 { + return res[0], err + } + return nil, err } // FindStashBoxScenesByFingerprints queries stash-box for scenes using every // scene's MD5/OSHASH checksum, or PHash, and returns results in the same order // as the input slice. -func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, sceneIDs []string) ([][]*models.ScrapedScene, error) { - ids, err := stringslice.StringSliceToIntSlice(sceneIDs) - if err != nil { - return nil, err - } - - var fingerprints []*graphql.FingerprintQueryInput - // map fingerprints to their scene index - fpToScene := make(map[string][]int) - phashToScene := make(map[int64][]int) - - if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { - qb := r.Scene() - - for index, sceneID := range ids { - scene, err := qb.Find(sceneID) - if err != nil { - return err - } - - if scene == nil { - return fmt.Errorf("scene with id %d not found", sceneID) - } - - if scene.Checksum.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: scene.Checksum.String, - Algorithm: graphql.FingerprintAlgorithmMd5, - }) - fpToScene[scene.Checksum.String] = append(fpToScene[scene.Checksum.String], index) - } - - if scene.OSHash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: scene.OSHash.String, - Algorithm: graphql.FingerprintAlgorithmOshash, - }) - fpToScene[scene.OSHash.String] = append(fpToScene[scene.OSHash.String], index) - } - - if scene.Phash.Valid { - phashStr := utils.PhashToString(scene.Phash.Int64) - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: phashStr, - Algorithm: graphql.FingerprintAlgorithmPhash, - }) - fpToScene[phashStr] = append(fpToScene[phashStr], index) - phashToScene[scene.Phash.Int64] = append(phashToScene[scene.Phash.Int64], index) - } - } - - return nil - }); err != nil { - return nil, err - } - - allScenes, err := c.findStashBoxScenesByFingerprints(ctx, fingerprints) - if err != nil { - return nil, err - } - - // set the matched scenes back in their original order - ret := make([][]*models.ScrapedScene, len(sceneIDs)) - for _, s := range allScenes { - var addedTo []int - - addScene := func(sceneIndexes []int) { - for _, index := range sceneIndexes { - if !intslice.IntInclude(addedTo, index) { - addedTo = append(addedTo, index) - ret[index] = append(ret[index], s) - } - } - } - - for _, fp := range s.Fingerprints { - addScene(fpToScene[fp.Hash]) - - // HACK - we really need stash-box to return specific hash-to-result sets - if fp.Algorithm == graphql.FingerprintAlgorithmPhash.String() { - hash, err := utils.StringToPhash(fp.Hash) - if err != nil { - continue - } - - for phash, sceneIndexes := range phashToScene { - if phashMatches(hash, phash) { - addScene(sceneIndexes) - } - } - } - } - } - - return ret, nil -} - -// FindStashBoxScenesByFingerprintsFlat queries stash-box for scenes using every -// scene's MD5/OSHASH checksum, or PHash, and returns results a flat slice. -func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneIDs []string) ([]*models.ScrapedScene, error) { - ids, err := stringslice.StringSliceToIntSlice(sceneIDs) - if err != nil { - return nil, err - } - - var fingerprints []*graphql.FingerprintQueryInput +func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, ids []int) ([][]*models.ScrapedScene, error) { + var fingerprints [][]*graphql.FingerprintQueryInput if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { qb := r.Scene() @@ -213,26 +105,31 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneI return fmt.Errorf("scene with id %d not found", sceneID) } + var sceneFPs []*graphql.FingerprintQueryInput + if scene.Checksum.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ Hash: scene.Checksum.String, Algorithm: graphql.FingerprintAlgorithmMd5, }) } if scene.OSHash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ Hash: scene.OSHash.String, Algorithm: graphql.FingerprintAlgorithmOshash, }) } if scene.Phash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: utils.PhashToString(scene.Phash.Int64), + phashStr := utils.PhashToString(scene.Phash.Int64) + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ + Hash: phashStr, Algorithm: graphql.FingerprintAlgorithmPhash, }) } + + fingerprints = append(fingerprints, sceneFPs) } return nil @@ -243,27 +140,29 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneI return c.findStashBoxScenesByFingerprints(ctx, fingerprints) } -func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, fingerprints []*graphql.FingerprintQueryInput) ([]*models.ScrapedScene, error) { - var ret []*models.ScrapedScene - for i := 0; i < len(fingerprints); i += 100 { - end := i + 100 - if end > len(fingerprints) { - end = len(fingerprints) +func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, scenes [][]*graphql.FingerprintQueryInput) ([][]*models.ScrapedScene, error) { + var ret [][]*models.ScrapedScene + for i := 0; i < len(scenes); i += 40 { + end := i + 40 + if end > len(scenes) { + end = len(scenes) } - scenes, err := c.client.FindScenesByFullFingerprints(ctx, fingerprints[i:end]) + scenes, err := c.client.FindScenesBySceneFingerprints(ctx, scenes[i:end]) if err != nil { return nil, err } - sceneFragments := scenes.FindScenesByFullFingerprints - - for _, s := range sceneFragments { - ss, err := c.sceneFragmentToScrapedScene(ctx, s) - if err != nil { - return nil, err + for _, sceneFragments := range scenes.FindScenesBySceneFingerprints { + var sceneResults []*models.ScrapedScene + for _, scene := range sceneFragments { + ss, err := c.sceneFragmentToScrapedScene(ctx, scene) + if err != nil { + return nil, err + } + sceneResults = append(sceneResults, ss) } - ret = append(ret, ss) + ret = append(ret, sceneResults) } } @@ -926,6 +825,19 @@ func (c Client) SubmitSceneDraft(ctx context.Context, sceneID int, endpoint stri } } + stashIDs, err := qb.GetStashIDs(sceneID) + if err != nil { + return err + } + var stashID *string + for _, v := range stashIDs { + if v.Endpoint == endpoint { + stashID = &v.StashID + break + } + } + draft.ID = stashID + return nil }); err != nil { return nil, err @@ -1011,6 +923,19 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf draft.Urls = urls } + stashIDs, err := pqb.GetStashIDs(performer.ID) + if err != nil { + return err + } + var stashID *string + for _, v := range stashIDs { + if v.Endpoint == endpoint { + stashID = &v.StashID + break + } + } + draft.ID = stashID + return nil }); err != nil { return nil, err diff --git a/pkg/sqlite/saved_filter.go b/pkg/sqlite/saved_filter.go index 8630a14a7..6c507bee3 100644 --- a/pkg/sqlite/saved_filter.go +++ b/pkg/sqlite/saved_filter.go @@ -81,6 +81,24 @@ func (qb *savedFilterQueryBuilder) Find(id int) (*models.SavedFilter, error) { return &ret, nil } +func (qb *savedFilterQueryBuilder) FindMany(ids []int, ignoreNotFound bool) ([]*models.SavedFilter, error) { + var filters []*models.SavedFilter + for _, id := range ids { + filter, err := qb.Find(id) + if err != nil { + return nil, err + } + + if filter == nil && !ignoreNotFound { + return nil, fmt.Errorf("filter with id %d not found", id) + } + + filters = append(filters, filter) + } + + return filters, nil +} + func (qb *savedFilterQueryBuilder) FindByMode(mode models.FilterMode) ([]*models.SavedFilter, error) { // exclude empty-named filters - these are the internal default filters @@ -108,3 +126,12 @@ func (qb *savedFilterQueryBuilder) FindDefault(mode models.FilterMode) (*models. return nil, nil } + +func (qb *savedFilterQueryBuilder) All() ([]*models.SavedFilter, error) { + var ret models.SavedFilters + if err := qb.query(selectAll(savedFilterTable), nil, &ret); err != nil { + return nil, err + } + + return []*models.SavedFilter(ret), nil +} diff --git a/ui/v2.5/index.html b/ui/v2.5/index.html index 5a1a71ce2..b25ebc3d0 100755 --- a/ui/v2.5/index.html +++ b/ui/v2.5/index.html @@ -14,7 +14,7 @@ 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/ --> - + Stash diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 67d128a9a..1793ccf36 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -48,7 +48,7 @@ "i18n-iso-countries": "^6.4.0", "intersection-observer": "^0.12.0", "localforage": "1.9.0", - "lodash": "^4.17.20", + "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "mousetrap-pause": "^1.0.0", "normalize-url": "^4.5.1", @@ -92,7 +92,7 @@ "@types/apollo-upload-client": "^14.1.0", "@types/classnames": "^2.2.11", "@types/fslightbox-react": "^1.4.0", - "@types/lodash": "^4.14.168", + "@types/lodash-es": "^4.17.6", "@types/mousetrap": "^1.6.5", "@types/node": "14.14.22", "@types/react": "17.0.31", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index aa28ecac4..f008e1d97 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,12 +1,11 @@ -import React, { useEffect } from "react"; +import React, { lazy, Suspense, useEffect, useState } from "react"; import { Route, Switch, useRouteMatch } from "react-router-dom"; import { IntlProvider, CustomFormats } from "react-intl"; import { Helmet } from "react-helmet"; -import { mergeWith } from "lodash"; +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 { library } from "@fortawesome/fontawesome-svg-core"; -import { fas } from "@fortawesome/free-solid-svg-icons"; import { initPolyfills } from "src/polyfills"; import locales from "src/locales"; @@ -15,41 +14,48 @@ import { flattenMessages } from "src/utils"; import Mousetrap from "mousetrap"; import MousetrapPause from "mousetrap-pause"; import { ErrorBoundary } from "./components/ErrorBoundary"; -import Galleries from "./components/Galleries/Galleries"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; -import Performers from "./components/Performers/Performers"; -import Recommendations from "./components/Recommendations/Recommendations"; -import Scenes from "./components/Scenes/Scenes"; -import { Settings } from "./components/Settings/Settings"; -import { Stats } from "./components/Stats"; -import Studios from "./components/Studios/Studios"; -import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser"; -import { SceneDuplicateChecker } from "./components/SceneDuplicateChecker/SceneDuplicateChecker"; -import Movies from "./components/Movies/Movies"; -import Tags from "./components/Tags/Tags"; -import Images from "./components/Images/Images"; -import { Setup } from "./components/Setup/Setup"; -import { Migrate } from "./components/Setup/Migrate"; import * as GQL from "./core/generated-graphql"; import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared"; + import { ConfigurationProvider } from "./hooks/Config"; -import { ManualProvider } from "./components/Help/Manual"; +import { ManualProvider } from "./components/Help/context"; import { InteractiveProvider } from "./hooks/Interactive/context"; +const Performers = lazy(() => import("./components/Performers/Performers")); +const FrontPage = lazy(() => import("./components/FrontPage/FrontPage")); +const Scenes = lazy(() => import("./components/Scenes/Scenes")); +const Settings = lazy(() => import("./components/Settings/Settings")); +const Stats = lazy(() => import("./components/Stats")); +const Studios = lazy(() => import("./components/Studios/Studios")); +const Galleries = lazy(() => import("./components/Galleries/Galleries")); + +const Movies = lazy(() => import("./components/Movies/Movies")); +const Tags = lazy(() => import("./components/Tags/Tags")); +const Images = lazy(() => import("./components/Images/Images")); +const Setup = lazy(() => import("./components/Setup/Setup")); +const Migrate = lazy(() => import("./components/Setup/Migrate")); + +const SceneFilenameParser = lazy( + () => import("./components/SceneFilenameParser/SceneFilenameParser") +); +const SceneDuplicateChecker = lazy( + () => import("./components/SceneDuplicateChecker/SceneDuplicateChecker") +); + initPolyfills(); MousetrapPause(Mousetrap); -// Set fontawesome/free-solid-svg as default fontawesome icons -library.add(fas); - const intlFormats: CustomFormats = { date: { long: { year: "numeric", month: "long", day: "numeric" }, }, }; +const defaultLocale = "en-GB"; + function languageMessageString(language: string) { return language.replace(/-/, ""); } @@ -57,25 +63,32 @@ function languageMessageString(language: string) { export const App: React.FC = () => { const config = useConfiguration(); const { data: systemStatusData } = useSystemStatus(); - const defaultLocale = "en-GB"; + const language = config.data?.configuration?.interface?.language ?? defaultLocale; - const defaultMessageLanguage = languageMessageString(defaultLocale); - const messageLanguage = languageMessageString(language); // use en-GB as default messages if any messages aren't found in the chosen language - const mergedMessages = mergeWith( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (locales as any)[defaultMessageLanguage], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (locales as any)[messageLanguage], - (objVal, srcVal) => { - if (srcVal === "") { - return objVal; - } - } - ); - const messages = flattenMessages(mergedMessages); + const [messages, setMessages] = useState<{}>(); + + useEffect(() => { + const setLocale = async () => { + const defaultMessageLanguage = languageMessageString(defaultLocale); + const messageLanguage = languageMessageString(language); + + const defaultMessages = await locales[defaultMessageLanguage](); + const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); + const chosenMessages = await locales[messageLanguage](); + mergeWith(mergedMessages, chosenMessages, (objVal, srcVal) => { + if (srcVal === "") { + return objVal; + } + }); + + setMessages(flattenMessages(mergedMessages)); + }; + + setLocale(); + }, [language]); const setupMatch = useRouteMatch(["/setup", "/migrate"]); @@ -118,52 +131,64 @@ export const App: React.FC = () => { } return ( - - - - - - - - - - - - - - - - - + }> + + + + + + + + + + + + + + + + + + ); } return ( - - - - - - - - {maybeRenderNavbar()} -
{renderContent()}
-
-
-
-
-
-
+ + + }> + + + + + {maybeRenderNavbar()} +
+ {renderContent()} +
+
+
+
+
+
+
+ + ) : null}
); }; diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index 030d581dc..2a970fdb1 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -19,6 +19,7 @@ import V0130 from "./versions/v0130.md"; import V0131 from "./versions/v0131.md"; import V0140 from "./versions/v0140.md"; import V0150 from "./versions/v0150.md"; +import V0160 from "./versions/v0160.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; // to avoid use of explicit any @@ -57,9 +58,9 @@ const Changelog: React.FC = () => { // after new release: // add entry to releases, using the current* fields // then update the current fields. - const currentVersion = stashVersion || "v0.15.0"; + const currentVersion = stashVersion || "v0.16.0"; const currentDate = buildDate; - const currentPage = V0150; + const currentPage = V0160; const releases: IStashRelease[] = [ { @@ -68,6 +69,11 @@ const Changelog: React.FC = () => { page: currentPage, defaultOpen: true, }, + { + version: "v0.15.0", + date: "2022-05-18", + page: V0150, + }, { version: "v0.14.0", date: "2022-04-11", diff --git a/ui/v2.5/src/components/Changelog/Version.tsx b/ui/v2.5/src/components/Changelog/Version.tsx index b274a7c3d..cd5b99442 100644 --- a/ui/v2.5/src/components/Changelog/Version.tsx +++ b/ui/v2.5/src/components/Changelog/Version.tsx @@ -1,3 +1,4 @@ +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"; @@ -33,7 +34,7 @@ const Version: React.FC = ({

} > @@ -205,3 +206,5 @@ export const GenerateDialog: React.FC = ({ ); }; + +export default GenerateDialog; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx index fad0ff7a0..8b04da91b 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx @@ -5,6 +5,11 @@ import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { multiValueSceneFields, SceneField, sceneFields } from "./constants"; import { ThreeStateBoolean } from "./ThreeStateBoolean"; +import { + faCheck, + faPencilAlt, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; interface IFieldOptionsEditor { options: GQL.IdentifyFieldOptions | undefined; @@ -148,10 +153,10 @@ const FieldOptionsEditor: React.FC = ({ return intl.formatMessage({ id: "actions.use_default" }); } if (value) { - return ; + return ; } - return ; + return ; } const defaultVal = defaultOptions?.fieldOptions?.find( @@ -212,7 +217,7 @@ const FieldOptionsEditor: React.FC = ({ className="minimal text-success" onClick={() => onEditOptions()} > - + ) : ( <> )} diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index 49714e51c..f5964ee97 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -20,6 +20,11 @@ import { Manual } from "src/components/Help/Manual"; import { IScraperSource } from "./constants"; import { OptionsEditor } from "./Options"; import { SourcesEditor, SourcesList } from "./Sources"; +import { + faCogs, + faFolderOpen, + faQuestionCircle, +} from "@fortawesome/free-solid-svg-icons"; const autoTagScraperID = "builtin_autotag"; @@ -167,7 +172,7 @@ export const IdentifyDialog: React.FC = ({ title={intl.formatMessage({ id: "actions.select_folders" })} onClick={() => onClick()} > - + @@ -403,7 +408,7 @@ export const IdentifyDialog: React.FC = ({ = ({ className="minimal help-button" onClick={() => onShowManual()} > - + } > diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx index 81d213115..9cc0c6a51 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx @@ -5,6 +5,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { IScraperSource } from "./constants"; import { OptionsEditor } from "./Options"; +import { + faCog, + faGripVertical, + faMinus, + faPencilAlt, + faPlus, +} from "@fortawesome/free-solid-svg-icons"; interface ISourceEditor { isNew: boolean; @@ -50,7 +57,7 @@ export const SourcesEditor: React.FC = ({ dialogClassName="identify-source-editor" modalProps={{ animation: false, size: "lg" }} show - icon={isNew ? "plus" : "pencil-alt"} + icon={isNew ? faPlus : faPencilAlt} header={intl.formatMessage( { id: headerMsgId }, { @@ -184,19 +191,19 @@ export const SourcesList: React.FC = ({ onMouseEnter={() => setMouseOverIndex(index)} onMouseLeave={() => setMouseOverIndex(undefined)} > - + {s.displayName}
@@ -208,7 +215,7 @@ export const SourcesList: React.FC = ({ className="minimal add-scraper-source-button" onClick={() => editSource()} > - + )} diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index c3eceae8b..78f433ea9 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -5,10 +5,16 @@ import * as GQL from "src/core/generated-graphql"; import { Modal } from "src/components/Shared"; import { getStashboxBase } from "src/utils"; import { FormattedMessage, useIntl } from "react-intl"; +import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; interface IProps { show: boolean; - entity: { name?: string | null; id: string; title?: string | null }; + entity: { + name?: string | null; + id: string; + title?: string | null; + stash_ids: { stash_id: string; endpoint: string }[]; + }; boxes: Pick[]; query: DocumentNode; onHide: () => void; @@ -59,9 +65,15 @@ export const SubmitStashBoxDraft: React.FC = ({ const handleSelectBox = (e: React.ChangeEvent) => setSelectedBox(Number.parseInt(e.currentTarget.value) ?? 0); + // If the scene has an attached stash_id from that endpoint, the operation will be an update + const isUpdate = + entity.stash_ids.find( + (id) => id.endpoint === boxes[selectedBox].endpoint + ) !== undefined; + return ( = ({ ))} - +
+ {isUpdate && ( + + + + )} + +
) : ( <> @@ -124,3 +150,5 @@ export const SubmitStashBoxDraft: React.FC = ({
); }; + +export default SubmitStashBoxDraft; diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx new file mode 100644 index 000000000..019429b3d --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -0,0 +1,177 @@ +import React, { useMemo } from "react"; +import { useIntl } from "react-intl"; +import { + FrontPageContent, + ICustomFilter, + ISavedFilterRow, +} from "src/core/config"; +import * as GQL from "src/core/generated-graphql"; +import { useFindSavedFilter } from "src/core/StashService"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; +import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; +import { MovieRecommendationRow } from "../Movies/MovieRecommendationRow"; +import { PerformerRecommendationRow } from "../Performers/PerformerRecommendationRow"; +import { SceneRecommendationRow } from "../Scenes/SceneRecommendationRow"; +import { StudioRecommendationRow } from "../Studios/StudioRecommendationRow"; +import { TagRecommendationRow } from "../Tags/TagRecommendationRow"; + +interface IFilter { + mode: GQL.FilterMode; + filter: ListFilterModel; + header: string; +} + +const RecommendationRow: React.FC = ({ mode, filter, header }) => { + function isTouchEnabled() { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; + } + + const isTouch = isTouchEnabled(); + + switch (mode) { + case GQL.FilterMode.Scenes: + return ( + + ); + case GQL.FilterMode.Studios: + return ( + + ); + case GQL.FilterMode.Movies: + return ( + + ); + case GQL.FilterMode.Performers: + return ( + + ); + case GQL.FilterMode.Galleries: + return ( + + ); + case GQL.FilterMode.Images: + return ( + + ); + case GQL.FilterMode.Tags: + return ( + + ); + default: + return <>; + } +}; + +interface ISavedFilterResults { + savedFilterID: string; +} + +const SavedFilterResults: React.FC = ({ + savedFilterID, +}) => { + const { loading, data } = useFindSavedFilter(savedFilterID.toString()); + + const filter = useMemo(() => { + if (!data?.findSavedFilter) return; + + const { mode, filter: filterJSON } = data.findSavedFilter; + + const ret = new ListFilterModel(mode); + ret.currentPage = 1; + ret.configureFromQueryParameters(JSON.parse(filterJSON)); + ret.randomSeed = -1; + return ret; + }, [data?.findSavedFilter]); + + if (loading || !data?.findSavedFilter || !filter) { + return <>; + } + + const { name, mode } = data.findSavedFilter; + + return ; +}; + +interface ICustomFilterProps { + customFilter: ICustomFilter; +} + +const CustomFilterResults: React.FC = ({ + customFilter, +}) => { + const intl = useIntl(); + + const filter = useMemo(() => { + const itemsPerPage = 25; + const ret = new ListFilterModel(customFilter.mode); + ret.sortBy = customFilter.sortBy; + ret.sortDirection = customFilter.direction; + ret.itemsPerPage = itemsPerPage; + ret.currentPage = 1; + ret.randomSeed = -1; + return ret; + }, [customFilter]); + + const header = customFilter.message + ? intl.formatMessage( + { id: customFilter.message.id }, + customFilter.message.values + ) + : customFilter.title ?? ""; + + return ( + + ); +}; + +interface IProps { + content: FrontPageContent; +} + +export const Control: React.FC = ({ content }) => { + switch (content.__typename) { + case "SavedFilter": + return ( + + ); + case "CustomFilter": + return ; + default: + return <>; + } +}; diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx new file mode 100644 index 000000000..cddebca9d --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useConfigureUI } from "src/core/StashService"; +import { LoadingIndicator } from "src/components/Shared"; +import { Button } from "react-bootstrap"; +import { FrontPageConfig } from "./FrontPageConfig"; +import { useToast } from "src/hooks"; +import { Control } from "./Control"; +import { ConfigurationContext } from "src/hooks/Config"; +import { + FrontPageContent, + generateDefaultFrontPageContent, + IUIConfig, +} from "src/core/config"; + +const FrontPage: React.FC = () => { + const intl = useIntl(); + const Toast = useToast(); + + const [isEditing, setIsEditing] = useState(false); + const [saving, setSaving] = useState(false); + + const [saveUI] = useConfigureUI(); + + const { configuration, loading } = React.useContext(ConfigurationContext); + + async function onUpdateConfig(content?: FrontPageContent[]) { + setIsEditing(false); + + if (!content) { + return; + } + + setSaving(true); + try { + await saveUI({ + variables: { + input: { + frontPageContent: content, + }, + }, + }); + } catch (e) { + Toast.error(e); + } + setSaving(false); + } + + if (loading || saving) { + return ; + } + + if (isEditing) { + return onUpdateConfig(content)} />; + } + + const ui = (configuration?.ui ?? {}) as IUIConfig; + + if (!ui.frontPageContent) { + const defaultContent = generateDefaultFrontPageContent(intl); + onUpdateConfig(defaultContent); + } + + const { frontPageContent } = ui; + + return ( +
+
+ {frontPageContent?.map((content: FrontPageContent, i) => ( + + ))} +
+
+ +
+
+ ); +}; + +export default FrontPage; diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx new file mode 100644 index 000000000..4bbf6a7c0 --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -0,0 +1,407 @@ +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 { Button, Form, Modal } from "react-bootstrap"; +import { + FilterMode, + FindSavedFiltersQuery, + SavedFilter, +} from "src/core/generated-graphql"; +import { ConfigurationContext } from "src/hooks/Config"; +import { + IUIConfig, + ISavedFilterRow, + ICustomFilter, + FrontPageContent, + generatePremadeFrontPageContent, +} from "src/core/config"; + +interface IAddSavedFilterModalProps { + onClose: (content?: FrontPageContent) => void; + existingSavedFilterIDs: string[]; + candidates: FindSavedFiltersQuery; +} + +const FilterModeToMessageID = { + [FilterMode.Galleries]: "galleries", + [FilterMode.Images]: "images", + [FilterMode.Movies]: "movies", + [FilterMode.Performers]: "performers", + [FilterMode.SceneMarkers]: "markers", + [FilterMode.Scenes]: "scenes", + [FilterMode.Studios]: "studios", + [FilterMode.Tags]: "tags", +}; + +function filterTitle(intl: IntlShape, f: Pick) { + return `${intl.formatMessage({ id: FilterModeToMessageID[f.mode] })}: ${ + f.name + }`; +} + +const AddContentModal: React.FC = ({ + onClose, + existingSavedFilterIDs, + candidates, +}) => { + const intl = useIntl(); + + const premadeFilterOptions = useMemo( + () => generatePremadeFrontPageContent(intl), + [intl] + ); + + const [contentType, setContentType] = useState( + "front_page.types.premade_filter" + ); + const [premadeFilterIndex, setPremadeFilterIndex] = useState< + number | undefined + >(0); + const [savedFilter, setSavedFilter] = useState(); + + function onTypeSelected(t: string) { + setContentType(t); + + switch (t) { + case "front_page.types.premade_filter": + setPremadeFilterIndex(0); + setSavedFilter(undefined); + break; + case "front_page.types.saved_filter": + setPremadeFilterIndex(undefined); + setSavedFilter(undefined); + break; + } + } + + function isValid() { + switch (contentType) { + case "front_page.types.premade_filter": + return premadeFilterIndex !== undefined; + case "front_page.types.saved_filter": + return savedFilter !== undefined; + } + + return false; + } + + const savedFilterOptions = useMemo(() => { + const ret = [ + { + value: "", + text: "", + }, + ].concat( + candidates.findSavedFilters + .filter((f) => { + // markers not currently supported + return ( + f.mode !== FilterMode.SceneMarkers && + !existingSavedFilterIDs.includes(f.id) + ); + }) + .map((f) => { + return { + value: f.id, + text: filterTitle(intl, f), + }; + }) + ); + + ret.sort((a, b) => { + return a.text.localeCompare(b.text); + }); + + return ret; + }, [candidates, existingSavedFilterIDs, intl]); + + function renderTypeSelect() { + const options = [ + "front_page.types.premade_filter", + "front_page.types.saved_filter", + ]; + return ( + + + + + onTypeSelected(e.target.value)} + className="btn-secondary" + > + {options.map((c) => ( + + ))} + + + ); + } + + function maybeRenderPremadeFiltersSelect() { + if (contentType !== "front_page.types.premade_filter") return; + + return ( + + + + + setPremadeFilterIndex(parseInt(e.target.value))} + className="btn-secondary" + > + {premadeFilterOptions.map((c, i) => ( + + ))} + + + ); + } + + function maybeRenderSavedFiltersSelect() { + if (contentType !== "front_page.types.saved_filter") return; + return ( + + + + + setSavedFilter(e.target.value)} + className="btn-secondary" + > + {savedFilterOptions.map((c) => ( + + ))} + + + ); + } + + function doAdd() { + switch (contentType) { + case "front_page.types.premade_filter": + onClose(premadeFilterOptions[premadeFilterIndex!]); + return; + case "front_page.types.saved_filter": + onClose({ + __typename: "SavedFilter", + savedFilterId: parseInt(savedFilter!), + }); + return; + } + + onClose(); + } + + return ( + onClose()}> + + + + +
+ {renderTypeSelect()} + {maybeRenderSavedFiltersSelect()} + {maybeRenderPremadeFiltersSelect()} +
+
+ + + + +
+ ); +}; + +interface IFilterRowProps { + content: FrontPageContent; + allSavedFilters: Pick[]; + onDelete: () => void; +} + +const ContentRow: React.FC = (props: IFilterRowProps) => { + const intl = useIntl(); + + function title() { + switch (props.content.__typename) { + case "SavedFilter": + const savedFilter = props.allSavedFilters.find( + (f) => + f.id === (props.content as ISavedFilterRow).savedFilterId.toString() + ); + if (!savedFilter) return ""; + return filterTitle(intl, savedFilter); + case "CustomFilter": + const asCustomFilter = props.content as ICustomFilter; + if (asCustomFilter.message) + return intl.formatMessage( + { id: asCustomFilter.message.id }, + asCustomFilter.message.values + ); + return asCustomFilter.title ?? ""; + } + } + + return ( +
+
+
+

{title()}

+
+ +
+
+ ); +}; + +interface IFrontPageConfigProps { + onClose: (content?: FrontPageContent[]) => void; +} + +export const FrontPageConfig: React.FC = ({ + onClose, +}) => { + const { configuration, loading } = React.useContext(ConfigurationContext); + + const ui = configuration?.ui as IUIConfig; + + const { data: allFilters, loading: loading2 } = useFindSavedFilters(); + + const [isAdd, setIsAdd] = useState(false); + const [currentContent, setCurrentContent] = useState([]); + const [dragIndex, setDragIndex] = useState(); + + useEffect(() => { + if (!allFilters?.findSavedFilters) { + return; + } + + if (ui?.frontPageContent) { + setCurrentContent(ui.frontPageContent); + } + }, [allFilters, ui]); + + function onDragStart(event: React.DragEvent, index: number) { + event.dataTransfer.effectAllowed = "move"; + setDragIndex(index); + } + + function onDragOver(event: React.DragEvent, index?: number) { + if (dragIndex !== undefined && index !== undefined && index !== dragIndex) { + const newFilters = [...currentContent]; + const moved = newFilters.splice(dragIndex, 1); + newFilters.splice(index, 0, moved[0]); + setCurrentContent(newFilters); + setDragIndex(index); + } + + event.dataTransfer.dropEffect = "move"; + event.preventDefault(); + } + + function onDragOverDefault(event: React.DragEvent) { + event.dataTransfer.dropEffect = "move"; + event.preventDefault(); + } + + function onDrop() { + // assume we've already set the temp filter list + // feed it up + setDragIndex(undefined); + } + + if (loading || loading2) { + return ; + } + + const existingSavedFilterIDs = currentContent + .filter((f) => f.__typename === "SavedFilter") + .map((f) => (f as ISavedFilterRow).savedFilterId.toString()); + + function addSavedFilter(content?: FrontPageContent) { + setIsAdd(false); + + if (!content) { + return; + } + + setCurrentContent([...currentContent, content]); + } + + function deleteSavedFilter(index: number) { + setCurrentContent(currentContent.filter((f, i) => i !== index)); + } + + return ( + <> + {isAdd && allFilters && ( + + )} +
+
+ {currentContent.map((content, index) => ( +
onDragStart(e, index)} + onDragEnter={(e) => onDragOver(e, index)} + onDrop={() => onDrop()} + > + deleteSavedFilter(index)} + /> +
+ ))} +
+
+ +
+
+
+
+ + +
+
+ + ); +}; diff --git a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx new file mode 100644 index 000000000..0b48434c0 --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx @@ -0,0 +1,24 @@ +import React, { PropsWithChildren } from "react"; + +interface IProps { + className?: string; + header: string; + link: JSX.Element; +} + +export const RecommendationRow: React.FC> = ({ + className, + header, + link, + children, +}) => ( +
+
+
+

{header}

+
+ {link} +
+ {children} +
+); diff --git a/ui/v2.5/src/components/Recommendations/styles.scss b/ui/v2.5/src/components/FrontPage/styles.scss similarity index 71% rename from ui/v2.5/src/components/Recommendations/styles.scss rename to ui/v2.5/src/components/FrontPage/styles.scss index ab8fb2ab7..6121131b8 100644 --- a/ui/v2.5/src/components/Recommendations/styles.scss +++ b/ui/v2.5/src/components/FrontPage/styles.scss @@ -6,6 +6,17 @@ padding-left: 0; padding-right: 0; } + + .recommendations-footer { + display: flex; + justify-content: right; + margin-bottom: 1em; + margin-top: 1em; + + button:not(:last-child) { + margin-right: 10px; + } + } } .no-recommendations { @@ -24,6 +35,25 @@ padding: 15px 0; } +.recommendations-container-edit { + .recommendation-row { + background-color: $secondary; + margin-bottom: 10px; + + &:not(.recommendation-row-add) { + cursor: grab; + } + } + + .recommendation-row-add .recommendation-row-head { + justify-content: center; + } + + .recommendation-row-head { + padding: 15px 10px; + } +} + .recommendation-row-head h2 { display: inline-flex; font-size: 1.25rem; @@ -41,10 +71,111 @@ .recommendations-container .studio-card hr, .recommendations-container .movie-card hr, -.recommendations-container .gallery-card hr { +.recommendations-container .gallery-card hr, +.recommendations-container .image-card hr, +.recommendations-container .tag-card hr { margin-top: auto; } +/* skeletons */ +.skeleton-card { + -webkit-animation: cardLoadingAnimation 2s infinite ease-in-out; + -moz-animation: cardLoadingAnimation 2s infinite ease-in-out; + -o-animation: cardLoadingAnimation 2s infinite ease-in-out; + animation: cardLoadingAnimation 2s infinite ease-in-out; + background-clip: border-box; + background-color: #30404d; + border: 1px solid rgba(0, 0, 0, 0.13); + border-radius: 3px; + box-shadow: 0 0 0 1px #10161a66, 0 0 #10161a00, 0 0 #10161a00; + display: flex; + flex-direction: column; + margin: 5px; + overflow: hidden; + padding: 0; + position: relative; + word-wrap: break-word; +} + +@keyframes cardLoadingAnimation { + 50% { + opacity: 0.5; + } +} + +.scene-skeleton { + max-width: 320px; + min-height: 365px; + min-width: 320px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 25.2rem; + min-width: 20rem; + } +} + +.movie-skeleton { + max-width: 240px; + min-height: 540px; + min-width: 240px; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 34rem; + min-width: 16rem; + } +} + +.performer-skeleton { + max-width: 20rem; + min-height: 39.1rem; + min-width: 20rem; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 33.1rem; + min-width: 16rem; + } +} + +.image-skeleton, +.gallery-skeleton { + max-width: 320px; + min-height: 403.5px; + min-width: 320px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 38.5rem; + min-width: 20rem; + } +} + +.studio-skeleton { + max-width: 360px; + min-height: 278px; + min-width: 360px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 19.8rem; + min-width: 20rem; + } +} + +.tag-skeleton { + max-width: 240px; + min-height: 365px; + min-width: 240px; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 26rem; + min-width: 16rem; + } +} + /* Slider */ .slick-slider { box-sizing: border-box; @@ -310,7 +441,6 @@ list-style: none; margin: 0; padding: 0; - position: absolute; text-align: center; width: 100%; } diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index bff3dd5f7..d4122be78 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -6,6 +6,7 @@ import { Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeleteGalleryDialogProps { selected: GQL.SlimGalleryDataFragment[]; @@ -114,7 +115,7 @@ export const DeleteGalleriesDialog: React.FC = ( return ( = ( if (GalleriestudioID !== updateStudioID) { updateStudioID = undefined; } - if (!_.isEqual(galleryPerformerIDs, updatePerformerIds)) { + if (!isEqual(galleryPerformerIDs, updatePerformerIds)) { updatePerformerIds = []; } - if (!_.isEqual(galleryTagIDs, updateTagIds)) { + if (!isEqual(galleryTagIDs, updateTagIds)) { updateTagIds = []; } if (gallery.organized !== updateOrganized) { @@ -229,7 +230,7 @@ export const EditGalleriesDialog: React.FC = ( return ( = (props) => { content={popoverContent} > @@ -62,7 +63,7 @@ export const GalleryCard: React.FC = (props) => { content={popoverContent} > @@ -113,7 +114,7 @@ export const GalleryCard: React.FC = (props) => { return (
); @@ -167,12 +168,16 @@ export const GalleryCard: React.FC = (props) => { } overlays={maybeRenderSceneStudioOverlay()} details={ - <> - {props.gallery.date} +
+ {props.gallery.date}

- +

- +
} popovers={maybeRenderPopoverButtonGroup()} selected={props.selected} diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index b9f2673dc..1ef62af37 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -21,6 +21,7 @@ import { GalleryImagesPanel } from "./GalleryImagesPanel"; import { GalleryAddPanel } from "./GalleryAddPanel"; import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; import { GalleryScenesPanel } from "./GalleryScenesPanel"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; interface IProps { gallery: GQL.GalleryDataFragment; @@ -116,7 +117,7 @@ export const GalleryPage: React.FC = ({ gallery }) => { className="minimal" title={intl.formatMessage({ id: "operations" })} > - + {gallery.path ? ( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index e256251e0..15343f7e4 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -8,7 +8,7 @@ import { mutateAddGalleryImages } from "src/core/StashService"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import { useIntl } from "react-intl"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; interface IGalleryAddProps { gallery: GQL.GalleryDataFragment; @@ -88,7 +88,7 @@ export const GalleryAddPanel: React.FC = ({ gallery }) => { onClick: addImages, isDisplayed: showWhenSelected, postRefetch: true, - icon: "plus" as IconProp, + icon: faPlus, }, ]; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index b85f1bf7f..e6711da47 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -34,6 +34,7 @@ import { useFormik } from "formik"; import { FormUtils, TextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; +import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; interface IProps { isVisible: boolean; @@ -314,7 +315,7 @@ export const GalleryEditPanel: React.FC< ))} onReloadScrapers()}> - + diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx index e58bdb9c8..1aa7fa2a5 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx @@ -8,7 +8,7 @@ import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import { useIntl } from "react-intl"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faMinus } from "@fortawesome/free-solid-svg-icons"; interface IGalleryDetailsProps { gallery: GQL.GalleryDataFragment; @@ -82,7 +82,7 @@ export const GalleryImagesPanel: React.FC = ({ onClick: removeImages, isDisplayed: showWhenSelected, postRefetch: true, - icon: "minus" as IconProp, + icon: faMinus, buttonVariant: "danger", }, ]; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index a0b82a67d..917e72b28 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -10,7 +10,7 @@ import { ScrapedInputGroupRow, ScrapedTextAreaRow, } from "src/components/Shared/ScrapeDialog"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { useStudioCreate, usePerformerCreate, @@ -235,7 +235,7 @@ export const GalleryScrapeDialog: React.FC = ( return; } - const ret = _.clone(idList); + const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index cb240f13d..83a8eef90 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { Table } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; @@ -84,7 +84,7 @@ export const GalleryList: React.FC = ({ const { count } = result.data.findGalleries; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindGalleries(filterCopy); diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index 827e66603..437eaeb94 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -1,37 +1,55 @@ import React, { FunctionComponent } from "react"; -import { FindGalleriesQueryResult } from "src/core/generated-graphql"; +import { useFindGalleries } from "src/core/StashService"; import Slider from "react-slick"; import { GalleryCard } from "./GalleryCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindGalleriesQueryResult; - header: String; - linkText: String; + header: string; } export const GalleryRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findGalleries.count; + const result = useFindGalleries(props.filter); + const cardCount = result.data?.findGalleries.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findGalleries.galleries.map((gallery) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findGalleries.galleries.map((g) => ( + + ))}
-
+
); }; diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 6a9ac5136..7c69ede5b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -38,3 +38,5 @@ export const GalleryViewer: React.FC = ({ galleryId }) => { ); }; + +export default GalleryViewer; diff --git a/ui/v2.5/src/components/Help/Manual.tsx b/ui/v2.5/src/components/Help/Manual.tsx index 501563472..83f787c2f 100644 --- a/ui/v2.5/src/components/Help/Manual.tsx +++ b/ui/v2.5/src/components/Help/Manual.tsx @@ -1,4 +1,4 @@ -import React, { useState, PropsWithChildren, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Modal, Container, Row, Col, Nav, Tab } from "react-bootstrap"; import Introduction from "src/docs/en/Introduction.md"; import Tasks from "src/docs/en/Tasks.md"; @@ -239,62 +239,4 @@ export const Manual: React.FC = ({ ); }; -interface IManualContextState { - openManual: (tab?: string) => void; -} - -export const ManualStateContext = React.createContext({ - openManual: () => {}, -}); - -export const ManualProvider: React.FC = ({ children }) => { - const [showManual, setShowManual] = useState(false); - const [manualLink, setManualLink] = useState(); - - function openManual(tab?: string) { - setManualLink(tab); - setShowManual(true); - } - - useEffect(() => { - if (manualLink) setManualLink(undefined); - }, [manualLink]); - - return ( - - setShowManual(false)} - defaultActiveTab={manualLink} - /> - {children} - - ); -}; - -interface IManualLink { - tab: string; -} - -export const ManualLink: React.FC> = ({ - tab, - children, -}) => { - const { openManual } = React.useContext(ManualStateContext); - - return ( - { - openManual(`${tab}.md`); - e.preventDefault(); - }} - > - {children} - - ); -}; +export default Manual; diff --git a/ui/v2.5/src/components/Help/context.tsx b/ui/v2.5/src/components/Help/context.tsx new file mode 100644 index 000000000..335838429 --- /dev/null +++ b/ui/v2.5/src/components/Help/context.tsx @@ -0,0 +1,73 @@ +import React, { + lazy, + PropsWithChildren, + Suspense, + useEffect, + useState, +} from "react"; + +const Manual = lazy(() => import("./Manual")); + +interface IManualContextState { + openManual: (tab?: string) => void; +} + +export const ManualStateContext = React.createContext({ + openManual: () => {}, +}); + +export const ManualProvider: React.FC = ({ children }) => { + const [showManual, setShowManual] = useState(false); + const [manualLink, setManualLink] = useState(); + + function openManual(tab?: string) { + setManualLink(tab); + setShowManual(true); + } + + useEffect(() => { + if (manualLink) setManualLink(undefined); + }, [manualLink]); + + return ( + + }> + {showManual && ( + setShowManual(false)} + defaultActiveTab={manualLink} + /> + )} + + {children} + + ); +}; + +interface IManualLink { + tab: string; +} + +export const ManualLink: React.FC> = ({ + tab, + children, +}) => { + const { openManual } = React.useContext(ManualStateContext); + + return ( + { + openManual(`${tab}.md`); + e.preventDefault(); + }} + > + {children} + + ); +}; diff --git a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx index ee939ae9b..059be9f36 100644 --- a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx @@ -6,6 +6,7 @@ import { Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeleteImageDialogProps { selected: GQL.SlimImageDataFragment[]; @@ -106,7 +107,7 @@ export const DeleteImagesDialog: React.FC = ( return ( = ( if (imageStudioID !== updateStudioID) { updateStudioID = undefined; } - if (!_.isEqual(imagePerformerIDs, updatePerformerIds)) { + if (!isEqual(imagePerformerIDs, updatePerformerIds)) { updatePerformerIds = []; } - if (!_.isEqual(imageTagIDs, updateTagIds)) { + if (!isEqual(imageTagIDs, updateTagIds)) { updateTagIds = []; } if (image.organized !== updateOrganized) { @@ -219,7 +220,7 @@ export const EditImagesDialog: React.FC = ( return ( void; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; onPreview?: (ev: MouseEvent) => void; } @@ -34,7 +40,7 @@ export const ImageCard: React.FC = ( content={popoverContent} > @@ -76,7 +82,7 @@ export const ImageCard: React.FC = ( content={popoverContent} > @@ -88,7 +94,7 @@ export const ImageCard: React.FC = ( return (
); @@ -146,7 +152,7 @@ export const ImageCard: React.FC = ( {props.onPreview ? (
) : undefined} diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index b4144c8f9..338e19e22 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -21,6 +21,7 @@ import { ImageFileInfoPanel } from "./ImageFileInfoPanel"; import { ImageEditPanel } from "./ImageEditPanel"; import { ImageDetailPanel } from "./ImageDetailPanel"; import { DeleteImagesDialog } from "../DeleteImagesDialog"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; interface IImageParams { id?: string; @@ -132,7 +133,7 @@ export const Image: React.FC = () => { className="minimal" title="Operations" > - + = ({ const { count } = result.data.findImages; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindImages(filterCopy); diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx new file mode 100644 index 000000000..55f7e7b78 --- /dev/null +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -0,0 +1,52 @@ +import React, { FunctionComponent } from "react"; +import { useFindImages } from "src/core/StashService"; +import Slider from "react-slick"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; +import { ImageCard } from "./ImageCard"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + header: string; +} + +export const ImageRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const result = useFindImages(props.filter); + const cardCount = result.data?.findImages.count; + + if (!result.loading && !cardCount) { + return null; + } + + return ( + + + + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findImages.images.map((i) => ( + + ))} +
+
+ ); +}; diff --git a/ui/v2.5/src/components/List/AddFilterDialog.tsx b/ui/v2.5/src/components/List/AddFilterDialog.tsx index a370fab76..d7c0325bc 100644 --- a/ui/v2.5/src/components/List/AddFilterDialog.tsx +++ b/ui/v2.5/src/components/List/AddFilterDialog.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import React, { useEffect, useRef, useState } from "react"; import { Button, Form, Modal } from "react-bootstrap"; import { CriterionModifier } from "src/core/generated-graphql"; @@ -80,13 +80,13 @@ export const AddFilterDialog: React.FC = ({ function onChangedModifierSelect( event: React.ChangeEvent ) { - const newCriterion = _.cloneDeep(criterion); + const newCriterion = cloneDeep(criterion); newCriterion.modifier = event.target.value as CriterionModifier; setCriterion(newCriterion); } function onValueChanged(value: CriterionValue) { - const newCriterion = _.cloneDeep(criterion); + const newCriterion = cloneDeep(criterion); newCriterion.value = value; setCriterion(newCriterion); } diff --git a/ui/v2.5/src/components/List/FilterTags.tsx b/ui/v2.5/src/components/List/FilterTags.tsx index db7a7063e..48e79da98 100644 --- a/ui/v2.5/src/components/List/FilterTags.tsx +++ b/ui/v2.5/src/components/List/FilterTags.tsx @@ -6,6 +6,7 @@ import { } from "src/models/list-filter/criteria/criterion"; import { useIntl } from "react-intl"; import { Icon } from "../Shared"; +import { faTimes } from "@fortawesome/free-solid-svg-icons"; interface IFilterTagsProps { criteria: Criterion[]; @@ -48,7 +49,7 @@ export const FilterTags: React.FC = ({ variant="secondary" onClick={($event) => onRemoveCriterionTag(criterion, $event)} > - + )); diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 1b6237aeb..9236a5561 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -1,4 +1,5 @@ -import _, { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; +import cloneDeep from "lodash-es/cloneDeep"; import React, { HTMLAttributes, useEffect, useRef, useState } from "react"; import cx from "classnames"; import Mousetrap from "mousetrap"; @@ -23,6 +24,15 @@ import { ListFilterOptions } from "src/models/list-filter/filter-options"; import { FormattedMessage, useIntl } from "react-intl"; import { PersistanceLevel } from "src/hooks/ListHook"; import { SavedFilterList } from "./SavedFilterList"; +import { + faBookmark, + faCaretDown, + faCaretUp, + faCheck, + faFilter, + faRandom, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; const maxPageSize = 1000; interface IListFilterProps { @@ -53,7 +63,7 @@ export const ListFilter: React.FC = ({ const [perPageInput, perPageFocus] = useFocus(); const searchCallback = debounce((value: string) => { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.searchTerm = value; newFilter.currentPage = 1; onFilterUpdate(newFilter); @@ -101,7 +111,7 @@ export const ListFilter: React.FC = ({ pp = maxPageSize; } - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.itemsPerPage = pp; newFilter.currentPage = 1; onFilterUpdate(newFilter); @@ -120,7 +130,7 @@ export const ListFilter: React.FC = ({ } function onChangeSortDirection() { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); if (filter.sortDirection === SortDirectionEnum.Asc) { newFilter.sortDirection = SortDirectionEnum.Desc; } else { @@ -131,14 +141,14 @@ export const ListFilter: React.FC = ({ } function onChangeSortBy(eventKey: string | null) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.sortBy = eventKey ?? undefined; newFilter.currentPage = 1; onFilterUpdate(newFilter); } function onReshuffleRandomSort() { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.currentPage = 1; newFilter.randomSeed = -1; onFilterUpdate(newFilter); @@ -207,28 +217,8 @@ export const ListFilter: React.FC = ({ return ( <> -
- - - - - - - } - > - - - - - - - +
+
= ({ queryClearShowing ? "" : "d-none" )} > - + - - - - - } - > - - - - +
- + + + + + + } + > + + + + + + + + + + } + > + + + + + {currentSortBy @@ -292,8 +301,8 @@ export const ListFilter: React.FC = ({ @@ -307,19 +316,19 @@ export const ListFilter: React.FC = ({ } > )} -
+
onChangePageSize(e.target.value)} value={filter.itemsPerPage.toString()} - className="btn-secondary mx-1 mb-1" + className="btn-secondary" > {pageSizeOptions.map((s) => (
@@ -143,7 +150,7 @@ export const QueueViewer: React.FC = ({
); }; + +export default QueueViewer; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx index bdec9167e..010c20c96 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; import { Button } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; +import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; +import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; export interface IRatingStarsProps { value?: number; @@ -33,20 +35,20 @@ export const RatingStars: React.FC = ( props.onSetRating(newRating); } - function getIconPrefix(rating: number) { + function getIcon(rating: number) { if (hoverRating && hoverRating >= rating) { if (hoverRating === props.value) { - return "far"; + return farStar; } - return "fas"; + return fasStar; } if (!hoverRating && props.value && props.value >= rating) { - return "fas"; + return fasStar; } - return "far"; + return farStar; } function onMouseOver(rating: number) { @@ -101,10 +103,7 @@ export const RatingStars: React.FC = ( title={getTooltip(rating)} key={`star-${rating}`} > - + ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 67a36991e..b036b2b17 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -1,6 +1,6 @@ import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import queryString from "query-string"; -import React, { useEffect, useState, useMemo, useContext } from "react"; +import React, { useEffect, useState, useMemo, useContext, lazy } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -16,29 +16,41 @@ import { queryFindScenes, queryFindScenesByID, } from "src/core/StashService"; -import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; -import { Icon } from "src/components/Shared"; + +import Icon from "src/components/Shared/Icon"; import { useToast } from "src/hooks"; -import { SubmitStashBoxDraft } from "src/components/Dialogs/SubmitDraft"; -import { ScenePlayer, getPlayerPosition } from "src/components/ScenePlayer"; +import SceneQueue from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import Mousetrap from "mousetrap"; -import { SceneQueue } from "src/models/sceneQueue"; -import { QueueViewer } from "./QueueViewer"; -import { SceneMarkersPanel } from "./SceneMarkersPanel"; -import { SceneFileInfoPanel } from "./SceneFileInfoPanel"; -import { SceneEditPanel } from "./SceneEditPanel"; -import { SceneDetailPanel } from "./SceneDetailPanel"; import { OCounterButton } from "./OCounterButton"; -import { ExternalPlayerButton } from "./ExternalPlayerButton"; -import { SceneMoviePanel } from "./SceneMoviePanel"; -import { SceneGalleriesPanel } from "./SceneGalleriesPanel"; -import { DeleteScenesDialog } from "../DeleteScenesDialog"; -import { GenerateDialog } from "../../Dialogs/GenerateDialog"; -import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { OrganizedButton } from "./OrganizedButton"; import { ConfigurationContext } from "src/hooks/Config"; +import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; + +const SubmitStashBoxDraft = lazy( + () => import("src/components/Dialogs/SubmitDraft") +); +const ScenePlayer = lazy( + () => import("src/components/ScenePlayer/ScenePlayer") +); + +const GalleryViewer = lazy( + () => import("src/components/Galleries/GalleryViewer") +); +const ExternalPlayerButton = lazy(() => import("./ExternalPlayerButton")); + +const QueueViewer = lazy(() => import("./QueueViewer")); +const SceneMarkersPanel = lazy(() => import("./SceneMarkersPanel")); +const SceneFileInfoPanel = lazy(() => import("./SceneFileInfoPanel")); +const SceneEditPanel = lazy(() => import("./SceneEditPanel")); +const SceneDetailPanel = lazy(() => import("./SceneDetailPanel")); +const SceneMoviePanel = lazy(() => import("./SceneMoviePanel")); +const SceneGalleriesPanel = lazy(() => import("./SceneGalleriesPanel")); +const DeleteScenesDialog = lazy(() => import("../DeleteScenesDialog")); +const GenerateDialog = lazy(() => import("../../Dialogs/GenerateDialog")); +const SceneVideoFilterPanel = lazy(() => import("./SceneVideoFilterPanel")); interface IProps { scene: GQL.SceneDataFragment; @@ -237,7 +249,7 @@ const ScenePage: React.FC = ({ className="minimal" title={intl.formatMessage({ id: "operations" })} > - + = (props) => { ); }; + +export default SceneDetailPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index f6f2e7781..94d9687b8 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState, useMemo, lazy } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Button, @@ -30,7 +30,7 @@ import { ImageInput, URLField, } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import useToast from "src/hooks/Toast"; import { ImageUtils, FormUtils, TextUtils, getStashIDs } from "src/utils"; import { MovieSelect } from "src/components/Shared/Select"; import { useFormik } from "formik"; @@ -39,8 +39,14 @@ import { ConfigurationContext } from "src/hooks/Config"; import { stashboxDisplayName } from "src/utils/stashbox"; import { SceneMovieTable } from "./SceneMovieTable"; import { RatingStars } from "./RatingStars"; -import { SceneScrapeDialog } from "./SceneScrapeDialog"; -import { SceneQueryModal } from "./SceneQueryModal"; +import { + faSearch, + faSyncAlt, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; + +const SceneScrapeDialog = lazy(() => import("./SceneScrapeDialog")); +const SceneQueryModal = lazy(() => import("./SceneQueryModal")); interface IProps { scene: GQL.SceneDataFragment; @@ -77,7 +83,11 @@ export const SceneEditPanel: React.FC = ({ const [coverImagePreview, setCoverImagePreview] = useState< string | undefined - >(scene.paths.screenshot ?? undefined); + >(); + + useEffect(() => { + setCoverImagePreview(scene.paths.screenshot ?? undefined); + }, [scene.paths.screenshot]); const { configuration: stashConfig } = React.useContext(ConfigurationContext); @@ -397,7 +407,7 @@ export const SceneEditPanel: React.FC = ({ return ( - + @@ -424,7 +434,7 @@ export const SceneEditPanel: React.FC = ({ ))} onReloadScrapers()}> - + @@ -496,7 +506,7 @@ export const SceneEditPanel: React.FC = ({ ))} onReloadScrapers()}> - + @@ -853,7 +863,7 @@ export const SceneEditPanel: React.FC = ({ )} onClick={() => removeStashID(stashID)} > - + {link} @@ -904,3 +914,5 @@ export const SceneEditPanel: React.FC = ({
); }; + +export default SceneEditPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx index a6d62f20f..f8c1c3063 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx @@ -175,3 +175,5 @@ export const SceneFileInfoPanel: React.FC = ( ); }; + +export default SceneFileInfoPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx index 30b8ba83d..4a8ca38f6 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx @@ -15,3 +15,5 @@ export const SceneGalleriesPanel: React.FC = ({ return
{cards}
; }; + +export default SceneGalleriesPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx index 352d42e36..2d24dfb8a 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx @@ -13,8 +13,8 @@ import { TagSelect, MarkerTitleSuggest, } from "src/components/Shared"; -import { getPlayerPosition } from "src/components/ScenePlayer"; -import { useToast } from "src/hooks"; +import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import useToast from "src/hooks/Toast"; interface IFormFields { title: string; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx index ec2e6653d..10b8228b6 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx @@ -89,3 +89,5 @@ export const SceneMarkersPanel: React.FC = (
); }; + +export default SceneMarkersPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx index e3df55308..6961c4761 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx @@ -23,3 +23,5 @@ export const SceneMoviePanel: FunctionComponent = ( ); }; + +export default SceneMoviePanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx index 560ffae31..1dfd4deed 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx @@ -10,7 +10,8 @@ import { Icon, } from "src/components/Shared"; import { queryScrapeSceneQuery } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import useToast from "src/hooks/Toast"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; interface ISceneSearchResultDetailsProps { scene: GQL.ScrapedSceneDataFragment; @@ -219,7 +220,7 @@ export const SceneQueryModal: React.FC = ({ variant="primary" title={intl.formatMessage({ id: "actions.search" })} > - + @@ -235,3 +236,5 @@ export const SceneQueryModal: React.FC = ({
); }; + +export default SceneQueryModal; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 96e462682..a3515940d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -10,7 +10,7 @@ import { ScrapedTextAreaRow, ScrapedImageRow, } from "src/components/Shared/ScrapeDialog"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { useStudioCreate, usePerformerCreate, @@ -18,8 +18,8 @@ import { useTagCreate, makePerformerCreateInput, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { DurationUtils } from "src/utils"; +import useToast from "src/hooks/Toast"; +import DurationUtils from "src/utils/duration"; import { useIntl } from "react-intl"; function renderScrapedStudio( @@ -297,7 +297,7 @@ export const SceneScrapeDialog: React.FC = ({ return; } - const ret = _.clone(idList); + const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); @@ -634,3 +634,5 @@ export const SceneScrapeDialog: React.FC = ({ /> ); }; + +export default SceneScrapeDialog; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx index af3beafac..f45c2bfae 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Button, Form } from "react-bootstrap"; -import { TruncatedText } from "src/components/Shared"; -import { VIDEO_PLAYER_ID } from "src/components/ScenePlayer"; +import TruncatedText from "src/components/Shared/TruncatedText"; +import { VIDEO_PLAYER_ID } from "src/components/ScenePlayer/util"; import * as GQL from "src/core/generated-graphql"; interface ISceneVideoFilterPanelProps { @@ -670,3 +670,5 @@ export const SceneVideoFilterPanel: React.FC = ( ); }; + +export default SceneVideoFilterPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index 143d552a0..7fe17d46a 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -1,9 +1,8 @@ import React, { useState } from "react"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { useIntl } from "react-intl"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FindScenesQueryResult, SlimSceneDataFragment, @@ -25,6 +24,7 @@ import { SceneCardsGrid } from "./SceneCardsGrid"; import { TaggerContext } from "../Tagger/context"; import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog"; import { ConfigurationContext } from "src/hooks/Config"; +import { faPlay } from "@fortawesome/free-solid-svg-icons"; interface ISceneList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -50,7 +50,7 @@ export const SceneList: React.FC = ({ text: intl.formatMessage({ id: "actions.play_selected" }), onClick: playSelected, isDisplayed: showWhenSelected, - icon: "play" as IconProp, + icon: faPlay, }, { text: intl.formatMessage({ id: "actions.play_random" }), @@ -137,7 +137,7 @@ export const SceneList: React.FC = ({ const indexMax = filter.itemsPerPage < count ? filter.itemsPerPage : count; const index = Math.floor(Math.random() * indexMax); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.currentPage = page; filterCopy.sortBy = "random"; const queryResults = await queryFindScenes(filterCopy); @@ -300,3 +300,5 @@ export const SceneList: React.FC = ({ return {listData.template}; }; + +export default SceneList; diff --git a/ui/v2.5/src/components/Scenes/SceneListTable.tsx b/ui/v2.5/src/components/Scenes/SceneListTable.tsx index d5b8f8f02..b4b7803b0 100644 --- a/ui/v2.5/src/components/Scenes/SceneListTable.tsx +++ b/ui/v2.5/src/components/Scenes/SceneListTable.tsx @@ -102,7 +102,7 @@ export const SceneListTable: React.FC = ( {scene.gallery && ( )} diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 97638338d..9958b0432 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import React from "react"; import { useHistory } from "react-router-dom"; import { useIntl } from "react-intl"; @@ -58,7 +58,7 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { const { count } = result.data.findSceneMarkers; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindSceneMarkers(filterCopy); @@ -98,3 +98,5 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { ); }; + +export default SceneMarkerList; diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx index ecd713d5b..2d9aa7371 100644 --- a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -1,45 +1,63 @@ -import React, { FunctionComponent } from "react"; -import { FindScenesQueryResult } from "src/core/generated-graphql"; +import React, { FunctionComponent, useMemo } from "react"; +import { useFindScenes } from "src/core/StashService"; import Slider from "react-slick"; import { SceneCard } from "./SceneCard"; import { SceneQueue } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindScenesQueryResult; - queue: SceneQueue; - header: String; - linkText: String; + header: string; } export const SceneRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findScenes.count; + const result = useFindScenes(props.filter); + const cardCount = result.data?.findScenes.count; + + const queue = useMemo(() => { + return SceneQueue.fromListFilterModel(props.filter); + }, [props.filter]); + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findScenes.scenes.map((scene, index) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findScenes.scenes.map((scene, index) => ( + + ))}
-
+
); }; diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index b49d5296c..ea2a7befe 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { lazy } 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 { PersistanceLevel } from "src/hooks/ListHook"; -import Scene from "./SceneDetails/Scene"; -import { SceneList } from "./SceneList"; -import { SceneMarkerList } from "./SceneMarkerList"; + +const SceneList = lazy(() => import("./SceneList")); +const SceneMarkerList = lazy(() => import("./SceneMarkerList")); +const Scene = lazy(() => import("./SceneDetails/Scene")); const Scenes: React.FC = () => { const intl = useIntl(); diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index a4c322f46..8e70b095e 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -1,3 +1,4 @@ +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; @@ -94,7 +95,7 @@ export const SettingGroup: React.FC> = ({ variant="minimal" onClick={() => setOpen(!open)} > - + ); } diff --git a/ui/v2.5/src/components/Settings/Settings.tsx b/ui/v2.5/src/components/Settings/Settings.tsx index bd2defc19..56d526bd7 100644 --- a/ui/v2.5/src/components/Settings/Settings.tsx +++ b/ui/v2.5/src/components/Settings/Settings.tsx @@ -148,3 +148,5 @@ export const Settings: React.FC = () => { ); }; + +export default Settings; diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index ecbf9e639..b3fde0c1a 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -98,7 +98,7 @@ export const SettingsInterfacePanel: React.FC = () => { - + diff --git a/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx b/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx index 55aaa6f48..a7a35237e 100644 --- a/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx @@ -5,6 +5,7 @@ import { SettingSection } from "./SettingSection"; import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs"; import { SettingStateContext } from "./context"; import { useIntl } from "react-intl"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; export const SettingsLibraryPanel: React.FC = () => { const intl = useIntl(); @@ -85,7 +86,7 @@ export const SettingsLibraryPanel: React.FC = () => { rel="noopener noreferrer" target="_blank" > - + } @@ -107,7 +108,7 @@ export const SettingsLibraryPanel: React.FC = () => { rel="noopener noreferrer" target="_blank" > - + } diff --git a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx index afb38b3f3..274c82b1c 100644 --- a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx @@ -8,6 +8,7 @@ import { TextUtils } from "src/utils"; import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared"; import { SettingSection } from "./SettingSection"; import { Setting, SettingGroup } from "./Inputs"; +import { faLink, faSyncAlt } from "@fortawesome/free-solid-svg-icons"; export const SettingsPluginsPanel: React.FC = () => { const Toast = useToast(); @@ -30,7 +31,7 @@ export const SettingsPluginsPanel: React.FC = () => { target="_blank" rel="noopener noreferrer" > - + ); @@ -105,7 +106,7 @@ export const SettingsPluginsPanel: React.FC = () => { @@ -364,7 +369,7 @@ export const SettingsServicesPanel: React.FC = () => { title={intl.formatMessage({ id: "actions.allow_temporarily" })} onClick={() => setTempIP(a)} > - + @@ -386,7 +391,7 @@ export const SettingsServicesPanel: React.FC = () => { onClick={() => setTempIP(ipEntry)} disabled={!ipEntry} > - + diff --git a/ui/v2.5/src/components/Settings/StashConfiguration.tsx b/ui/v2.5/src/components/Settings/StashConfiguration.tsx index 04b484b1e..57676b5fc 100644 --- a/ui/v2.5/src/components/Settings/StashConfiguration.tsx +++ b/ui/v2.5/src/components/Settings/StashConfiguration.tsx @@ -1,3 +1,4 @@ +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Form, Row, Col, Dropdown } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; @@ -72,7 +73,7 @@ const Stash: React.FC = ({ id={`stash-menu-${index}`} className="minimal" > - + onEdit()}> diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx index 684aec6d3..663eaa77c 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -15,10 +15,16 @@ import { ImportDialog } from "./ImportDialog"; import * as GQL from "src/core/generated-graphql"; import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting } from "../Inputs"; -import { ManualLink } from "src/components/Help/Manual"; +import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared"; import { ConfigurationContext } from "src/hooks/Config"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; +import { + faMinus, + faPlus, + faQuestionCircle, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; interface ICleanDialog { pathSelection?: boolean; @@ -63,7 +69,7 @@ const CleanDialog: React.FC = ({ return ( = ({ title={intl.formatMessage({ id: "actions.delete" })} onClick={() => removePath(p)} > - + @@ -103,7 +109,7 @@ const CleanDialog: React.FC = ({ variant="secondary" onClick={() => addPath(currentDirectory)} > - + } /> @@ -188,7 +194,7 @@ export const DataManagementTasks: React.FC = ({ return ( = ({ <> - + } diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index a3ba44c03..84b1e0d5a 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -1,3 +1,8 @@ +import { + faMinus, + faPencilAlt, + faPlus, +} from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Col, Form, Row } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -41,7 +46,7 @@ export const DirectorySelectionDialog: React.FC show modalProps={{ animation }} disabled={!allowEmpty && paths.length === 0} - icon="pencil-alt" + icon={faPencilAlt} header={intl.formatMessage({ id: "actions.select_folders" })} accept={{ onClick: () => { @@ -69,7 +74,7 @@ export const DirectorySelectionDialog: React.FC title={intl.formatMessage({ id: "actions.delete" })} onClick={() => removePath(p)} > - + @@ -84,7 +89,7 @@ export const DirectorySelectionDialog: React.FC variant="secondary" onClick={() => addPath(currentDirectory)} > - + } /> diff --git a/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx index 9a73dc693..ff5025089 100644 --- a/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx @@ -5,6 +5,7 @@ import { Modal } from "src/components/Shared"; import * as GQL from "src/core/generated-graphql"; import { useToast } from "src/hooks"; import { useIntl } from "react-intl"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; interface IImportDialogProps { onClose: () => void; @@ -115,7 +116,7 @@ export const ImportDialog: React.FC = ( return ( { diff --git a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx index ba6a04b80..b88107434 100644 --- a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx @@ -7,8 +7,15 @@ import { } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useIntl } from "react-intl"; +import { + faBan, + faCheck, + faCircle, + faCog, + faHourglassStart, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -68,25 +75,25 @@ const Task: React.FC = ({ job }) => { } function getStatusIcon() { - let icon: IconProp = "circle"; + let icon = faCircle; let iconClass = ""; switch (job.status) { case GQL.JobStatus.Ready: - icon = "hourglass-start"; + icon = faHourglassStart; break; case GQL.JobStatus.Running: - icon = "cog"; + icon = faCog; iconClass = "fa-spin"; break; case GQL.JobStatus.Stopping: - icon = "cog"; + icon = faCog; iconClass = "fa-spin"; break; case GQL.JobStatus.Finished: - icon = "check"; + icon = faCheck; break; case GQL.JobStatus.Cancelled: - icon = "ban"; + icon = faBan; break; } @@ -138,7 +145,7 @@ const Task: React.FC = ({ job }) => { onClick={() => stopJob()} disabled={!canStop()} > - +
diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index eb9045336..82c4c3b17 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -17,8 +17,9 @@ import { useToast } from "src/hooks"; import { GenerateOptions } from "./GenerateOptions"; import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting, SettingGroup } from "../Inputs"; -import { ManualLink } from "src/components/Help/Manual"; +import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; interface IAutoTagOptions { options: GQL.AutoTagMetadataInput; @@ -296,7 +297,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), @@ -335,7 +336,7 @@ export const LibraryTasks: React.FC = () => { <> - + } @@ -358,7 +359,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), @@ -399,7 +400,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), diff --git a/ui/v2.5/src/components/Settings/context.tsx b/ui/v2.5/src/components/Settings/context.tsx index 84e45ef70..757d548a0 100644 --- a/ui/v2.5/src/components/Settings/context.tsx +++ b/ui/v2.5/src/components/Settings/context.tsx @@ -1,5 +1,9 @@ import { ApolloError } from "@apollo/client/errors"; -import { debounce } from "lodash"; +import { + faCheckCircle, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons"; +import debounce from "lodash-es/debounce"; import React, { useState, useEffect, @@ -8,6 +12,7 @@ import React, { useRef, } from "react"; import { Spinner } from "react-bootstrap"; +import { IUIConfig } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; import { useConfiguration, @@ -16,6 +21,7 @@ import { useConfigureGeneral, useConfigureInterface, useConfigureScraping, + useConfigureUI, } from "src/core/StashService"; import { useToast } from "src/hooks"; import { withoutTypename } from "src/utils"; @@ -29,6 +35,7 @@ export interface ISettingsContextState { defaults: GQL.ConfigDefaultSettingsInput; scraping: GQL.ConfigScrapingInput; dlna: GQL.ConfigDlnaInput; + ui: IUIConfig; // apikey isn't directly settable, so expose it here apiKey: string; @@ -38,6 +45,7 @@ export interface ISettingsContextState { saveDefaults: (input: Partial) => void; saveScraping: (input: Partial) => void; saveDLNA: (input: Partial) => void; + saveUI: (input: IUIConfig) => void; } export const SettingStateContext = React.createContext({ @@ -48,12 +56,14 @@ export const SettingStateContext = React.createContext({ defaults: {}, scraping: {}, dlna: {}, + ui: {}, apiKey: "", saveGeneral: () => {}, saveInterface: () => {}, saveDefaults: () => {}, saveScraping: () => {}, saveDLNA: () => {}, + saveUI: () => {}, }); export const SettingsContext: React.FC = ({ children }) => { @@ -92,6 +102,10 @@ export const SettingsContext: React.FC = ({ children }) => { >(); const [updateDLNAConfig] = useConfigureDLNA(); + const [ui, setUI] = useState({}); + const [pendingUI, setPendingUI] = useState<{} | undefined>(); + const [updateUIConfig] = useConfigureUI(); + const [updateSuccess, setUpdateSuccess] = useState(); const [apiKey, setApiKey] = useState(""); @@ -121,6 +135,7 @@ export const SettingsContext: React.FC = ({ children }) => { setDefaults({ ...withoutTypename(data.configuration.defaults) }); setScraping({ ...withoutTypename(data.configuration.scraping) }); setDLNA({ ...withoutTypename(data.configuration.dlna) }); + setUI({ ...withoutTypename(data.configuration.ui) }); setApiKey(data.configuration.general.apiKey); }, [data, error]); @@ -387,11 +402,61 @@ export const SettingsContext: React.FC = ({ children }) => { }); } + // saves the configuration if no further changes are made after a half second + const saveUIConfig = useMemo( + () => + debounce(async (input: IUIConfig) => { + try { + setUpdateSuccess(undefined); + await updateUIConfig({ + variables: { + input, + }, + }); + + setPendingUI(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, 500), + [updateUIConfig, onSuccess] + ); + + useEffect(() => { + if (!pendingUI) { + return; + } + + saveUIConfig(pendingUI); + }, [pendingUI, saveUIConfig]); + + function saveUI(input: IUIConfig) { + if (!ui) { + return; + } + + setUI({ + ...ui, + ...input, + }); + + setPendingUI((current) => { + if (!current) { + return input; + } + return { + ...current, + ...input, + }; + }); + } + function maybeRenderLoadingIndicator() { if (updateSuccess === false) { return (
- +
); } @@ -401,7 +466,8 @@ export const SettingsContext: React.FC = ({ children }) => { pendingInterface || pendingDefaults || pendingScraping || - pendingDLNA + pendingDLNA || + pendingUI ) { return (
@@ -415,7 +481,7 @@ export const SettingsContext: React.FC = ({ children }) => { if (updateSuccess) { return (
- +
); } @@ -432,11 +498,13 @@ export const SettingsContext: React.FC = ({ children }) => { defaults, scraping, dlna, + ui, saveGeneral, saveInterface, saveDefaults, saveScraping, saveDLNA, + saveUI, }} > {maybeRenderLoadingIndicator()} diff --git a/ui/v2.5/src/components/SettingsButton.tsx b/ui/v2.5/src/components/SettingsButton.tsx index a15203da5..b8f9e2a5a 100644 --- a/ui/v2.5/src/components/SettingsButton.tsx +++ b/ui/v2.5/src/components/SettingsButton.tsx @@ -4,6 +4,7 @@ import { Button } from "react-bootstrap"; import { useJobQueue, useJobsSubscribe } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { useIntl } from "react-intl"; +import { faCog } from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -59,7 +60,7 @@ export const SettingsButton: React.FC = () => { className="minimal d-flex align-items-center h-100" title={intl.formatMessage({ id: "settings" })} > - 0} /> + 0} /> ); }; diff --git a/ui/v2.5/src/components/Setup/Migrate.tsx b/ui/v2.5/src/components/Setup/Migrate.tsx index 788481ca8..6dd6851ec 100644 --- a/ui/v2.5/src/components/Setup/Migrate.tsx +++ b/ui/v2.5/src/components/Setup/Migrate.tsx @@ -180,3 +180,5 @@ export const Migrate: React.FC = () => { ); }; + +export default Migrate; diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index 066c24047..16a10fcd5 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -15,6 +15,11 @@ import { ConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; import { Icon, LoadingIndicator, Modal } from "../Shared"; import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog"; +import { + faEllipsisH, + faExclamationTriangle, + faQuestionCircle, +} from "@fortawesome/free-solid-svg-icons"; export const Setup: React.FC = () => { const { configuration, loading: configLoading } = useContext( @@ -108,7 +113,7 @@ export const Setup: React.FC = () => { return ( { className="text-input" onClick={() => setShowGeneratedDialog(true)} > - + @@ -528,7 +533,7 @@ export const Setup: React.FC = () => {

}} + values={{ icon: }} />

@@ -640,3 +645,5 @@ export const Setup: React.FC = () => { ); }; + +export default Setup; diff --git a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx index ba292d1a7..a64754d61 100644 --- a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx +++ b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx @@ -1,7 +1,8 @@ +import { faBan } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, Form, FormControlProps, InputGroup } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Icon } from "."; +import Icon from "./Icon"; interface IBulkUpdateTextInputProps extends FormControlProps { valueChanged: (value: string | undefined) => void; @@ -37,7 +38,7 @@ export const BulkUpdateTextInput: React.FC = ({ onClick={() => valueChanged(undefined)} title={intl.formatMessage({ id: "actions.unset" })} > - + ) : undefined} diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index e29097821..2216121de 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -1,6 +1,10 @@ +import { + faChevronDown, + faChevronRight, +} from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface IProps { text: string; @@ -17,7 +21,7 @@ export const CollapseButton: React.FC> = ( onClick={() => setOpen(!open)} className="minimal collapse-button" > - + {props.text} diff --git a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx index fe5cc4922..cf6284982 100644 --- a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx +++ b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { FetchResult } from "@apollo/client"; -import { Modal } from "src/components/Shared"; +import Modal from "src/components/Shared/Modal"; import { useToast } from "src/hooks"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeletionEntity { id: string; @@ -78,7 +79,7 @@ const DeleteEntityDialog: React.FC = ({ return ( = (props: IProps) => { disabled={props.disabled} onClick={() => increment()} > - + ); @@ -86,7 +91,7 @@ export const DurationInput: React.FC = (props: IProps) => { if (props.onReset) { return ( ); } diff --git a/ui/v2.5/src/components/Shared/ExportDialog.tsx b/ui/v2.5/src/components/Shared/ExportDialog.tsx index 6e49b740a..3c0ad5b7c 100644 --- a/ui/v2.5/src/components/Shared/ExportDialog.tsx +++ b/ui/v2.5/src/components/Shared/ExportDialog.tsx @@ -1,11 +1,12 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { mutateExportObjects } from "src/core/StashService"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { downloadFile } from "src/utils"; +import Modal from "src/components/Shared/Modal"; +import useToast from "src/hooks/Toast"; +import downloadFile from "src/utils/download"; import { ExportObjectsInput } from "src/core/generated-graphql"; import { useIntl } from "react-intl"; +import { faCogs } from "@fortawesome/free-solid-svg-icons"; interface IExportDialogProps { exportInput: ExportObjectsInput; @@ -47,7 +48,7 @@ export const ExportDialog: React.FC = ( return ( = ({ {loading ? ( ) : ( - + )} ) : undefined} diff --git a/ui/v2.5/src/components/Shared/Icon.tsx b/ui/v2.5/src/components/Shared/Icon.tsx index 533477575..d1f2616f6 100644 --- a/ui/v2.5/src/components/Shared/Icon.tsx +++ b/ui/v2.5/src/components/Shared/Icon.tsx @@ -1,17 +1,9 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, SizeProp, library } from "@fortawesome/fontawesome-svg-core"; -import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; -import { - faCheckCircle as farCheckCircle, - faStar as farStar, -} from "@fortawesome/free-regular-svg-icons"; - -// need these to use far and fas styles of stars -library.add(fasStar, farStar, farCheckCircle); +import { IconDefinition, SizeProp } from "@fortawesome/fontawesome-svg-core"; interface IIcon { - icon: IconProp; + icon: IconDefinition; className?: string; color?: string; size?: SizeProp; diff --git a/ui/v2.5/src/components/Shared/ImageInput.tsx b/ui/v2.5/src/components/Shared/ImageInput.tsx index 4b6183779..f1ace6f83 100644 --- a/ui/v2.5/src/components/Shared/ImageInput.tsx +++ b/ui/v2.5/src/components/Shared/ImageInput.tsx @@ -8,8 +8,9 @@ import { Row, } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Modal } from "."; +import Modal from "./Modal"; import Icon from "./Icon"; +import { faFile, faLink } from "@fortawesome/free-solid-svg-icons"; interface IImageInput { isEditing: boolean; @@ -100,7 +101,7 @@ export const ImageInput: React.FC = ({

= ({
diff --git a/ui/v2.5/src/components/Shared/Modal.tsx b/ui/v2.5/src/components/Shared/Modal.tsx index 1ff9a1a50..8cf3b8029 100644 --- a/ui/v2.5/src/components/Shared/Modal.tsx +++ b/ui/v2.5/src/components/Shared/Modal.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Button, Modal, Spinner, ModalProps } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; -import { IconName } from "@fortawesome/fontawesome-svg-core"; +import Icon from "src/components/Shared/Icon"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { FormattedMessage } from "react-intl"; interface IButton { @@ -14,7 +14,7 @@ interface IModal { show: boolean; onHide?: () => void; header?: string; - icon?: IconName; + icon?: IconDefinition; cancel?: IButton; accept?: IButton; isRunning?: boolean; diff --git a/ui/v2.5/src/components/Shared/OperationButton.tsx b/ui/v2.5/src/components/Shared/OperationButton.tsx index a7c23dcf2..c71d9e139 100644 --- a/ui/v2.5/src/components/Shared/OperationButton.tsx +++ b/ui/v2.5/src/components/Shared/OperationButton.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import { Button, ButtonProps } from "react-bootstrap"; -import { LoadingIndicator } from "src/components/Shared"; +import LoadingIndicator from "src/components/Shared/LoadingIndicator"; interface IOperationButton extends ButtonProps { operation?: () => Promise; diff --git a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx index f669281a8..d12da10d3 100644 --- a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx +++ b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx @@ -1,3 +1,4 @@ +import { faUser } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { Link } from "react-router-dom"; @@ -36,7 +37,7 @@ export const PerformerPopoverButton: React.FC = ({ performers }) => { content={popoverContent} > diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index d7e3904d1..3eb1c731f 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -1,3 +1,9 @@ +import { + faFilm, + faImage, + faImages, + faPlayCircle, +} from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -24,13 +30,13 @@ export const PopoverCountButton: React.FC = ({ function getIcon() { switch (type) { case "scene": - return "play-circle"; + return faPlayCircle; case "image": - return "image"; + return faImage; case "gallery": - return "images"; + return faImages; case "movie": - return "film"; + return faFilm; } } diff --git a/ui/v2.5/src/components/Shared/RatingStars.tsx b/ui/v2.5/src/components/Shared/RatingStars.tsx index 0847dba0b..3b26fc96e 100644 --- a/ui/v2.5/src/components/Shared/RatingStars.tsx +++ b/ui/v2.5/src/components/Shared/RatingStars.tsx @@ -1,3 +1,5 @@ +import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; +import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; import React from "react"; import Icon from "./Icon"; @@ -12,21 +14,21 @@ interface IProps { export const RatingStars: React.FC = ({ rating }) => rating ? (
- + = 2 ? "fas" : "far", "star"]} + icon={rating >= 2 ? fasStar : farStar} className={rating >= 2 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} /> = 3 ? "fas" : "far", "star"]} + icon={rating >= 3 ? fasStar : farStar} className={rating >= 3 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} /> = 4 ? "fas" : "far", "star"]} + icon={rating >= 4 ? fasStar : farStar} className={rating >= 4 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} />
diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx index ecaa3bf00..a81f86981 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx @@ -8,9 +8,18 @@ import { FormControl, Badge, } from "react-bootstrap"; -import { CollapseButton, Icon, Modal } from "src/components/Shared"; -import _ from "lodash"; +import { CollapseButton } from "src/components/Shared/CollapseButton"; +import Icon from "src/components/Shared/Icon"; +import Modal from "src/components/Shared/Modal"; +import isEqual from "lodash-es/isEqual"; +import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; +import { + faCheck, + faPencilAlt, + faPlus, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; export class ScrapeResult { public newValue?: T; @@ -22,7 +31,7 @@ export class ScrapeResult { this.originalValue = originalValue ?? undefined; this.newValue = newValue ?? undefined; - const valuesEqual = _.isEqual(originalValue, newValue); + const valuesEqual = isEqual(originalValue, newValue); this.useNewValue = !!this.newValue && !valuesEqual; this.scraped = this.useNewValue; } @@ -33,11 +42,14 @@ export class ScrapeResult { } public cloneWithValue(value?: T) { - const ret = _.clone(this); + const ret = clone(this); ret.newValue = value; - ret.useNewValue = !_.isEqual(ret.newValue, ret.originalValue); - ret.scraped = ret.useNewValue; + ret.useNewValue = !isEqual(ret.newValue, ret.originalValue); + + // #2691 - if we're setting the value, assume it should be treated as + // scraped + ret.scraped = true; return ret; } @@ -73,7 +85,7 @@ function renderButtonIcon(selected: boolean) { return ( ); } @@ -82,7 +94,7 @@ export const ScrapeDialogRow = ( props: IScrapedRowProps ) => { function handleSelectClick(isNew: boolean) { - const ret = _.clone(props.result); + const ret = clone(props.result); ret.useNewValue = isNew; props.onChange(ret); } @@ -111,7 +123,7 @@ export const ScrapeDialogRow = ( > {t.name} ))} @@ -344,7 +356,7 @@ export const ScrapeDialog: React.FC = ( return ( { diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 64790341d..bc494cfc9 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -8,7 +8,7 @@ import Select, { OptionsType, } from "react-select"; import CreatableSelect from "react-select/creatable"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import * as GQL from "src/core/generated-graphql"; import { diff --git a/ui/v2.5/src/components/Shared/StringListInput.tsx b/ui/v2.5/src/components/Shared/StringListInput.tsx index 1c4e7937d..95c4bac50 100644 --- a/ui/v2.5/src/components/Shared/StringListInput.tsx +++ b/ui/v2.5/src/components/Shared/StringListInput.tsx @@ -1,6 +1,7 @@ +import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, Form, InputGroup } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface IStringListInputProps { value: string[]; @@ -49,7 +50,7 @@ export const StringListInput: React.FC = (props) => { /> @@ -57,7 +58,7 @@ export const StringListInput: React.FC = (props) => { )}
{props.errors}
diff --git a/ui/v2.5/src/components/Shared/SuccessIcon.tsx b/ui/v2.5/src/components/Shared/SuccessIcon.tsx index 292dae0ed..79d56d979 100644 --- a/ui/v2.5/src/components/Shared/SuccessIcon.tsx +++ b/ui/v2.5/src/components/Shared/SuccessIcon.tsx @@ -1,12 +1,13 @@ +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import React from "react"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface ISuccessIconProps { className?: string; } const SuccessIcon: React.FC = ({ className }) => ( - + ); export default SuccessIcon; diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 8b28e646b..e5e7d8fb7 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -10,7 +10,8 @@ import { SceneDataFragment, GalleryDataFragment, } from "src/core/generated-graphql"; -import { NavUtils, TextUtils } from "src/utils"; +import NavUtils from "src/utils/navigation"; +import TextUtils from "src/utils/text"; interface IProps { tag?: Partial; diff --git a/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx b/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx index 9340b4114..3e7cb8ac5 100644 --- a/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx +++ b/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx @@ -1,6 +1,7 @@ +import { faCheck, faMinus, faTimes } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; -import { Icon } from "."; +import Icon from "./Icon"; interface IThreeStateCheckbox { value: boolean | undefined; @@ -28,7 +29,7 @@ export const ThreeStateCheckbox: React.FC = ({ return true; } - const icon = value === undefined ? "minus" : value ? "check" : "times"; + const icon = value === undefined ? faMinus : value ? faCheck : faTimes; const labelClassName = value === undefined ? "unset" : value ? "checked" : "not-checked"; diff --git a/ui/v2.5/src/components/Shared/TruncatedText.tsx b/ui/v2.5/src/components/Shared/TruncatedText.tsx index 4c450403c..fd0c925b9 100644 --- a/ui/v2.5/src/components/Shared/TruncatedText.tsx +++ b/ui/v2.5/src/components/Shared/TruncatedText.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState } from "react"; import { Overlay, Tooltip } from "react-bootstrap"; import { Placement } from "react-bootstrap/Overlay"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import cx from "classnames"; const CLASSNAME = "TruncatedText"; diff --git a/ui/v2.5/src/components/Shared/URLField.tsx b/ui/v2.5/src/components/Shared/URLField.tsx index cc233f9d9..d9818ddef 100644 --- a/ui/v2.5/src/components/Shared/URLField.tsx +++ b/ui/v2.5/src/components/Shared/URLField.tsx @@ -1,8 +1,9 @@ import React from "react"; import { useIntl } from "react-intl"; import { Button, InputGroup, Form } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { FormikHandlers } from "formik"; +import { faFileDownload } from "@fortawesome/free-solid-svg-icons"; interface IProps { value: string; @@ -36,7 +37,7 @@ export const URLField: React.FC = (props: IProps) => { disabled={!props.value || !props.urlScrapable(props.value)} title={intl.formatMessage({ id: "actions.scrape" })} > - + diff --git a/ui/v2.5/src/components/Stats.tsx b/ui/v2.5/src/components/Stats.tsx index bfc1ff01b..69007f122 100644 --- a/ui/v2.5/src/components/Stats.tsx +++ b/ui/v2.5/src/components/Stats.tsx @@ -121,3 +121,5 @@ export const Stats: React.FC = () => {
); }; + +export default Stats; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 56875b40c..6a43ce54e 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -28,6 +28,7 @@ import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioEditPanel } from "./StudioEditPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IProps { studio: GQL.StudioDataFragment; @@ -112,7 +113,7 @@ const StudioPage: React.FC = ({ studio }) => { return ( ; @@ -197,7 +198,7 @@ export const StudioEditPanel: React.FC = ({ )} onClick={() => removeStashID(stashID)} > - + {link} diff --git a/ui/v2.5/src/components/Studios/StudioList.tsx b/ui/v2.5/src/components/Studios/StudioList.tsx index 794ac1046..7e14190df 100644 --- a/ui/v2.5/src/components/Studios/StudioList.tsx +++ b/ui/v2.5/src/components/Studios/StudioList.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; import { @@ -67,7 +67,7 @@ export const StudioList: React.FC = ({ const { count } = result.data.findStudios; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindStudios(filterCopy); diff --git a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx index ea26e7fbe..a35337daf 100644 --- a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx +++ b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx @@ -1,37 +1,55 @@ import React, { FunctionComponent } from "react"; -import { FindStudiosQueryResult } from "src/core/generated-graphql"; +import { useFindStudios } from "src/core/StashService"; import Slider from "react-slick"; import { StudioCard } from "./StudioCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindStudiosQueryResult; - header: String; - linkText: String; + header: string; } export const StudioRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findStudios.count; + const result = useFindStudios(props.filter); + const cardCount = result.data?.findStudios.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findStudios.studios.map((studio) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findStudios.studios.map((s) => ( + + ))}
-
+ ); }; diff --git a/ui/v2.5/src/components/Tagger/IncludeButton.tsx b/ui/v2.5/src/components/Tagger/IncludeButton.tsx index bc0e12ab6..b60e7e12d 100644 --- a/ui/v2.5/src/components/Tagger/IncludeButton.tsx +++ b/ui/v2.5/src/components/Tagger/IncludeButton.tsx @@ -1,3 +1,4 @@ +import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { Icon } from "../Shared"; @@ -21,7 +22,7 @@ export const IncludeExcludeButton: React.FC = ({ exclude ? "text-danger" : "text-success" } include-exclude-button`} > - + ); diff --git a/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx b/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx index c7635529f..3862ea7ac 100644 --- a/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx @@ -1,3 +1,4 @@ +import { faCheck, faList, faTimes } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Row, Col } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -36,7 +37,7 @@ const PerformerFieldSelect: React.FC = ({ variant="secondary" className={excluded[name] ? "text-muted" : "text-success"} > - + {TextUtils.capitalize(name)} @@ -45,7 +46,7 @@ const PerformerFieldSelect: React.FC = ({ return ( void; excludedPerformerFields?: string[]; header: string; - icon: IconName; + icon: IconDefinition; create?: boolean; endpoint?: string; } @@ -91,7 +98,7 @@ const PerformerModal: React.FC = ({ variant="secondary" className={excluded[name] ? "text-muted" : "text-success"} > - + )} @@ -219,7 +226,7 @@ const PerformerModal: React.FC = ({
Stash-Box Source - +
)} @@ -236,7 +243,7 @@ const PerformerModal: React.FC = ({ excluded.image ? "text-muted" : "text-success" )} > - + )} = ({
Select performer image @@ -265,7 +272,7 @@ const PerformerModal: React.FC = ({ {imageIndex + 1} of {images.length}
diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index 2d3f8a27e..2a1a1f4b7 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -20,6 +20,7 @@ import PerformerConfig from "./Config"; import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "../constants"; import PerformerModal from "../PerformerModal"; import { useUpdatePerformer } from "../queries"; +import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -352,7 +353,7 @@ const PerformerTaggerList: React.FC = ({ performer={modalPerformer} onSave={handlePerformerUpdate} excludedPerformerFields={config.excludedPerformerFields} - icon="tags" + icon={faTags} header={intl.formatMessage({ id: "performer_tagger.update_performer", })} @@ -381,7 +382,7 @@ const PerformerTaggerList: React.FC = ({ = ({ = ({ modalVisible={modalPerformer !== undefined} performer={modalPerformer} onSave={handleSave} - icon="tags" + icon={faTags} header="Update Performer" excludedPerformerFields={excludedPerformerFields} endpoint={endpoint} diff --git a/ui/v2.5/src/components/Tagger/queries.ts b/ui/v2.5/src/components/Tagger/queries.ts index 9daa3b1b5..69bf6d116 100644 --- a/ui/v2.5/src/components/Tagger/queries.ts +++ b/ui/v2.5/src/components/Tagger/queries.ts @@ -1,5 +1,5 @@ import * as GQL from "src/core/generated-graphql"; -import { sortBy } from "lodash"; +import sortBy from "lodash-es/sortBy"; export const useUpdatePerformerStashID = () => { const [updatePerformer] = GQL.usePerformerUpdateMutation({ diff --git a/ui/v2.5/src/components/Tagger/scenes/Config.tsx b/ui/v2.5/src/components/Tagger/scenes/Config.tsx index 1c1702687..2eaa94a43 100644 --- a/ui/v2.5/src/components/Tagger/scenes/Config.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/Config.tsx @@ -1,3 +1,4 @@ +import { faTimes } from "@fortawesome/free-solid-svg-icons"; import React, { useRef, useContext } from "react"; import { Badge, @@ -205,7 +206,7 @@ const Config: React.FC = ({ show }) => { className="minimal ml-2" onClick={() => removeBlacklist(index)} > - + ))} diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index dc0ca049a..12bbef5d9 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -11,6 +11,7 @@ import { ValidTypes, } from "src/components/Shared"; import { OptionalField } from "../IncludeButton"; +import { faSave } from "@fortawesome/free-solid-svg-icons"; interface IPerformerResultProps { performer: GQL.ScrapedPerformer; @@ -91,7 +92,7 @@ const PerformerResult: React.FC = ({ operation={onLink} hideChildrenWhenLoading > - + ); } diff --git a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index 1f26bad31..e793bb63a 100755 --- a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -12,6 +12,7 @@ import { TaggerScene } from "./TaggerScene"; import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneSearchResults } from "./StashSearchResult"; import { ConfigurationContext } from "src/hooks/Config"; +import { faCog } from "@fortawesome/free-solid-svg-icons"; interface ITaggerProps { scenes: GQL.SlimSceneDataFragment[]; @@ -83,7 +84,7 @@ export const Tagger: React.FC = ({ scenes, queue }) => { return (
); diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index 7f39deac1..33b6887f9 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import cx from "classnames"; import { Badge, Button, Col, Form, Row } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { uniq } from "lodash"; +import uniq from "lodash-es/uniq"; import { blobToBase64 } from "base64-blob"; import { distance } from "src/utils/hamming"; @@ -24,6 +24,7 @@ import { SceneTaggerModalsState } from "./sceneTaggerModals"; import PerformerResult from "./PerformerResult"; import StudioResult from "./StudioResult"; import { useInitialState } from "src/hooks/state"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; const getDurationStatus = ( scene: IScrapedScene, @@ -612,7 +613,7 @@ const StashSearchResult: React.FC = ({ > {t.name} ))} diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx index 35edf62c2..4ce707297 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -1,10 +1,11 @@ import React, { useContext } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import * as GQL from "src/core/generated-graphql"; import { Icon, Modal, TruncatedText } from "src/components/Shared"; import { TaggerStateContext } from "../context"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; interface IStudioModalProps { studio: GQL.ScrapedSceneStudioDataFragment; @@ -12,7 +13,7 @@ interface IStudioModalProps { closeModal: () => void; handleStudioCreate: (input: GQL.StudioCreateInput) => void; header: string; - icon: IconName; + icon: IconDefinition; } const StudioModal: React.FC = ({ @@ -93,7 +94,7 @@ const StudioModal: React.FC = ({
Stash-Box Source - +
)} diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx index 0c0d898bb..25a97c112 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx @@ -12,6 +12,7 @@ import { import * as GQL from "src/core/generated-graphql"; import { OptionalField } from "../IncludeButton"; +import { faSave } from "@fortawesome/free-solid-svg-icons"; interface IStudioResultProps { studio: GQL.ScrapedStudio; @@ -89,7 +90,7 @@ const StudioResult: React.FC = ({ operation={onLink} hideChildrenWhenLoading > - + ); } diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 137b62f18..87755c3b5 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -14,6 +14,7 @@ import { import { parsePath, prepareQueryString } from "src/components/Tagger/utils"; import { ScenePreview } from "src/components/Scenes/SceneCard"; import { TaggerStateContext } from "../context"; +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; interface ITaggerSceneDetails { scene: GQL.SlimSceneDataFragment; @@ -71,7 +72,7 @@ const TaggerSceneDetails: React.FC = ({ scene }) => { className="minimal collapse-button" size="lg" > - + ); diff --git a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx index 0e40d408a..fe670e32b 100644 --- a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx @@ -5,6 +5,7 @@ import StudioModal from "./StudioModal"; import PerformerModal from "../PerformerModal"; import { TaggerStateContext } from "../context"; import { useIntl } from "react-intl"; +import { faTags } from "@fortawesome/free-solid-svg-icons"; type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void; type StudioModalCallback = (toCreate?: GQL.StudioCreateInput) => void; @@ -112,7 +113,7 @@ export const SceneTaggerModals: React.FC = ({ children }) => { modalVisible performer={performerToCreate} onSave={handlePerformerSave} - icon="tags" + icon={faTags} header={intl.formatMessage( { id: "actions.create_entity" }, { entityType: intl.formatMessage({ id: "performer" }) } @@ -127,7 +128,7 @@ export const SceneTaggerModals: React.FC = ({ children }) => { modalVisible studio={studioToCreate} handleStudioCreate={handleStudioSave} - icon="tags" + icon={faTags} header={intl.formatMessage( { id: "actions.create_entity" }, { entityType: intl.formatMessage({ id: "studio" }) } diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 8daa7bfcf..3bb2418ec 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -7,6 +7,7 @@ import { FormattedMessage } from "react-intl"; import { Icon } from "../Shared"; import { GridCard } from "../Shared/GridCard"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; +import { faMapMarkerAlt, faUser } from "@fortawesome/free-solid-svg-icons"; interface IProps { tag: GQL.TagDataFragment; @@ -102,7 +103,7 @@ export const TagCard: React.FC = ({ return ( @@ -141,7 +142,7 @@ export const TagCard: React.FC = ({ return ( diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 47bb279f8..b5dc4618b 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -30,6 +30,11 @@ import { TagGalleriesPanel } from "./TagGalleriesPanel"; import { TagDetailsPanel } from "./TagDetailsPanel"; import { TagEditPanel } from "./TagEditPanel"; import { TagMergeModal } from "./TagMergeDialog"; +import { + faSignInAlt, + faSignOutAlt, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; interface IProps { tag: GQL.TagDataFragment; @@ -165,7 +170,7 @@ const TagPage: React.FC = ({ tag }) => { return ( = ({ tag }) => { className="bg-secondary text-white" onClick={() => setMergeType("from")} > - + ...
@@ -227,7 +232,7 @@ const TagPage: React.FC = ({ tag }) => { className="bg-secondary text-white" onClick={() => setMergeType("into")} > - + ... diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx index 891c2d228..a2c51383f 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx @@ -7,6 +7,7 @@ import { useTagsMerge } from "src/core/StashService"; import { useIntl } from "react-intl"; import { useToast } from "src/hooks"; import { useHistory } from "react-router-dom"; +import { faSignInAlt, faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; interface ITagMergeModalProps { show: boolean; @@ -74,7 +75,7 @@ export const TagMergeModal: React.FC = ({ onMerge(), diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 88e804a82..e6201064f 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import Mousetrap from "mousetrap"; import { FindTagsQueryResult } from "src/core/generated-graphql"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -25,6 +25,7 @@ import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared"; import { TagCard } from "./TagCard"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -82,7 +83,7 @@ export const TagList: React.FC = ({ filterHook }) => { const { count } = result.data.findTags; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindTags(filterCopy); @@ -240,7 +241,7 @@ export const TagList: React.FC = ({ filterHook }) => { {}} show={!!deletingTag} - icon="trash-alt" + icon={faTrashAlt} accept={{ onClick: onDelete, variant: "danger", @@ -338,7 +339,7 @@ export const TagList: React.FC = ({ filterHook }) => { /> diff --git a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx new file mode 100644 index 000000000..c684bc2c4 --- /dev/null +++ b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx @@ -0,0 +1,52 @@ +import React, { FunctionComponent } from "react"; +import { useFindTags } from "src/core/StashService"; +import Slider from "react-slick"; +import { TagCard } from "./TagCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + header: string; +} + +export const TagRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const result = useFindTags(props.filter); + const cardCount = result.data?.findTags.count; + + if (!result.loading && !cardCount) { + return null; + } + + return ( + + + + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findTags.tags.map((p) => ( + + ))} +
+
+ ); +}; diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index ce6aa8e78..1bb7734df 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -6,7 +6,7 @@ import { getOperationName, } from "@apollo/client/utilities"; import { stringToGender } from "src/utils/gender"; -import { filterData } from "../utils"; +import { filterData } from "../utils/data"; import { ListFilterModel } from "../models/list-filter/filter"; import * as GQL from "./generated-graphql"; @@ -44,7 +44,14 @@ const deleteCache = (queries: DocumentNode[]) => { }); }; -export const useFindSavedFilters = (mode: GQL.FilterMode) => +export const useFindSavedFilter = (id: string) => + GQL.useFindSavedFilterQuery({ + variables: { + id, + }, + }); + +export const useFindSavedFilters = (mode?: GQL.FilterMode) => GQL.useFindSavedFiltersQuery({ variables: { mode, @@ -813,6 +820,12 @@ export const useConfigureDefaults = () => update: deleteCache([GQL.ConfigurationDocument]), }); +export const useConfigureUI = () => + GQL.useConfigureUiMutation({ + refetchQueries: getQueryNames([GQL.ConfigurationDocument]), + update: deleteCache([GQL.ConfigurationDocument]), + }); + export const useJobsSubscribe = () => GQL.useJobsSubscribeSubscription(); export const useConfigureDLNA = () => diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts new file mode 100644 index 000000000..007d70e32 --- /dev/null +++ b/ui/v2.5/src/core/config.ts @@ -0,0 +1,88 @@ +import { IntlShape } from "react-intl"; +import { ITypename } from "src/utils"; +import { FilterMode, SortDirectionEnum } from "./generated-graphql"; + +// NOTE: double capitals aren't converted correctly in the backend + +export interface ISavedFilterRow extends ITypename { + __typename: "SavedFilter"; + savedFilterId: number; +} + +export interface IMessage { + id: string; + values: { [key: string]: string }; +} + +export interface ICustomFilter extends ITypename { + __typename: "CustomFilter"; + message?: IMessage; + title?: string; + mode: FilterMode; + sortBy: string; + direction: SortDirectionEnum; +} + +export type FrontPageContent = ISavedFilterRow | ICustomFilter; + +export interface IUIConfig { + frontPageContent?: FrontPageContent[]; +} + +function recentlyReleased( + intl: IntlShape, + mode: FilterMode, + objectsID: string +): ICustomFilter { + return { + __typename: "CustomFilter", + message: { + id: "recently_released_objects", + values: { objects: intl.formatMessage({ id: objectsID }) }, + }, + mode, + sortBy: "date", + direction: SortDirectionEnum.Desc, + }; +} + +function recentlyAdded( + intl: IntlShape, + mode: FilterMode, + objectsID: string +): ICustomFilter { + return { + __typename: "CustomFilter", + message: { + id: "recently_added_objects", + values: { objects: intl.formatMessage({ id: objectsID }) }, + }, + mode, + sortBy: "created_at", + direction: SortDirectionEnum.Desc, + }; +} + +export function generateDefaultFrontPageContent(intl: IntlShape) { + return [ + recentlyReleased(intl, FilterMode.Scenes, "scenes"), + recentlyAdded(intl, FilterMode.Studios, "studios"), + recentlyReleased(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Performers, "performers"), + recentlyReleased(intl, FilterMode.Galleries, "galleries"), + ]; +} + +export function generatePremadeFrontPageContent(intl: IntlShape) { + return [ + recentlyReleased(intl, FilterMode.Scenes, "scenes"), + recentlyAdded(intl, FilterMode.Scenes, "scenes"), + recentlyReleased(intl, FilterMode.Galleries, "galleries"), + recentlyAdded(intl, FilterMode.Galleries, "galleries"), + recentlyAdded(intl, FilterMode.Images, "images"), + recentlyReleased(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Studios, "studios"), + recentlyAdded(intl, FilterMode.Performers, "performers"), + ]; +} diff --git a/ui/v2.5/src/docs/en/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/KeyboardShortcuts.md index 022f59a5a..50026de0a 100644 --- a/ui/v2.5/src/docs/en/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/KeyboardShortcuts.md @@ -64,6 +64,9 @@ | `p n` | Play next scene in queue | | `p p` | Play previous scene in queue | | `p r` | Play random scene in queue | +| `{1-9}` | Seek to 10-90% duration | +| `[` | Scrub backwards 10% duration | +| `]` | Scrub forwards 10% duration | ### Scene Markers tab shortcuts diff --git a/ui/v2.5/src/hooks/Interactive/status.tsx b/ui/v2.5/src/hooks/Interactive/status.tsx index 268fca7b8..d630cbb9c 100644 --- a/ui/v2.5/src/hooks/Interactive/status.tsx +++ b/ui/v2.5/src/hooks/Interactive/status.tsx @@ -1,3 +1,4 @@ +import { faCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -35,7 +36,7 @@ export const SceneInteractiveStatus: React.FC = ({}) => { return (
- + {error && : {error}} diff --git a/ui/v2.5/src/hooks/Interval.ts b/ui/v2.5/src/hooks/Interval.ts index 854747c2b..838ea9764 100644 --- a/ui/v2.5/src/hooks/Interval.ts +++ b/ui/v2.5/src/hooks/Interval.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import noop from "lodash/noop"; +import noop from "lodash-es/noop"; const MIN_VALID_INTERVAL = 1000; diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index a82ef6f81..91fcffcc8 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -10,7 +10,7 @@ import { } from "react-bootstrap"; import cx from "classnames"; import Mousetrap from "mousetrap"; -import debounce from "lodash/debounce"; +import debounce from "lodash-es/debounce"; import { Icon, LoadingIndicator } from "src/components/Shared"; import { useInterval, usePageVisibility, useToast } from "src/hooks"; @@ -29,6 +29,19 @@ import { import * as GQL from "src/core/generated-graphql"; import { useInterfaceLocalForage } from "../LocalForage"; import { imageLightboxDisplayModeIntlMap } from "src/core/enums"; +import { ILightboxImage } from "./types"; +import { + faArrowLeft, + faArrowRight, + faChevronLeft, + faChevronRight, + faCog, + faExpand, + faPause, + faPlay, + faSearchMinus, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; const CLASSNAME = "Lightbox"; const CLASSNAME_HEADER = `${CLASSNAME}-header`; @@ -53,18 +66,6 @@ const DEFAULT_SLIDESHOW_DELAY = 5000; const SECONDS_TO_MS = 1000; const MIN_VALID_INTERVAL_SECONDS = 1; -interface IImagePaths { - image?: GQL.Maybe; - thumbnail?: GQL.Maybe; -} -export interface ILightboxImage { - id?: string; - title?: GQL.Maybe; - rating?: GQL.Maybe; - o_counter?: GQL.Maybe; - paths: IImagePaths; -} - interface IProps { images: ILightboxImage[]; isVisible: boolean; @@ -643,7 +644,7 @@ export const LightboxComponent: React.FC = ({ })} onClick={() => setShowOptions(!showOptions)} > - + = ({ onClick={toggleSlideshow} title="Toggle Slideshow" > - + )} {zoom !== 1 && ( @@ -691,7 +692,7 @@ export const LightboxComponent: React.FC = ({ }} title="Reset zoom" > - + )} {document.fullscreenEnabled && ( @@ -700,11 +701,11 @@ export const LightboxComponent: React.FC = ({ onClick={toggleFullscreen} title="Toggle Fullscreen" > - + )}
@@ -715,7 +716,7 @@ export const LightboxComponent: React.FC = ({ onClick={handleLeft} className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`} > - + )} @@ -757,7 +758,7 @@ export const LightboxComponent: React.FC = ({ onClick={handleRight} className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`} > - + )} @@ -768,7 +769,7 @@ export const LightboxComponent: React.FC = ({ onClick={() => setIndex(images.length - 1)} className={CLASSNAME_NAVBUTTON} > - + {navItems} )} @@ -813,3 +814,5 @@ export const LightboxComponent: React.FC = ({ ); }; + +export default LightboxComponent; diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx index c8e9bc106..2f857d5fb 100644 --- a/ui/v2.5/src/hooks/Lightbox/context.tsx +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -1,5 +1,7 @@ -import React, { useCallback, useState } from "react"; -import { ILightboxImage, LightboxComponent } from "./Lightbox"; +import React, { lazy, Suspense, useCallback, useState } from "react"; +import { ILightboxImage } from "./types"; + +const LightboxComponent = lazy(() => import("./Lightbox")); export interface IState { images: ILightboxImage[]; @@ -48,9 +50,11 @@ const Lightbox: React.FC = ({ children }) => { return ( {children} - {lightboxState.isVisible && ( - - )} + }> + {lightboxState.isVisible && ( + + )} + ); }; diff --git a/ui/v2.5/src/hooks/Lightbox/types.ts b/ui/v2.5/src/hooks/Lightbox/types.ts new file mode 100644 index 000000000..70bd16454 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/types.ts @@ -0,0 +1,14 @@ +import * as GQL from "src/core/generated-graphql"; + +interface IImagePaths { + image?: GQL.Maybe; + thumbnail?: GQL.Maybe; +} + +export interface ILightboxImage { + id?: string; + title?: GQL.Maybe; + rating?: GQL.Maybe; + o_counter?: GQL.Maybe; + paths: IImagePaths; +} diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index 5f95cf010..732610096 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -1,4 +1,6 @@ -import _ from "lodash"; +import clone from "lodash-es/clone"; +import cloneDeep from "lodash-es/cloneDeep"; +import isEqual from "lodash-es/isEqual"; import queryString from "query-string"; import React, { useCallback, @@ -10,7 +12,7 @@ import React, { import { ApolloError } from "@apollo/client"; import { useHistory, useLocation } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { SlimSceneDataFragment, SceneMarkerDataFragment, @@ -99,7 +101,7 @@ export interface IListHookOperation { selectedIds: Set ) => boolean; postRefetch?: boolean; - icon?: IconProp; + icon?: IconDefinition; buttonVariant?: string; } @@ -168,7 +170,7 @@ interface IRenderListProps { filter: ListFilterModel; filterOptions: ListFilterOptions; onChangePage: (page: number) => void; - updateQueryParams: (filter: ListFilterModel) => void; + updateFilter: (filter: ListFilterModel) => void; } const RenderList = < @@ -189,7 +191,7 @@ const RenderList = < selectable, renderEditDialog, renderDeleteDialog, - updateQueryParams, + updateFilter, filterDialog, persistState, }: IListHookOptions & @@ -268,7 +270,7 @@ const RenderList = < function singleSelect(id: string, selected: boolean) { setLastClickedId(id); - const newSelectedIds = _.clone(selectedIds); + const newSelectedIds = clone(selectedIds); if (selected) { newSelectedIds.add(id); } else { @@ -339,13 +341,13 @@ const RenderList = < } function onChangeZoom(newZoomIndex: number) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.zoomIndex = newZoomIndex; - updateQueryParams(newFilter); + updateFilter(newFilter); } - async function onOperationClicked(o: IListHookOperation) { - await o.onClick(result, filter, selectedIds); + function onOperationClicked(o: IListHookOperation) { + o.onClick(result, filter, selectedIds); if (o.postRefetch) { result.refetch(); } @@ -434,16 +436,16 @@ const RenderList = < } function onChangeDisplayMode(displayMode: DisplayMode) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.displayMode = displayMode; - updateQueryParams(newFilter); + updateFilter(newFilter); } function onAddCriterion( criterion: Criterion, oldId?: string ) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); // Find if we are editing an existing criteria, then modify that. Or create a new one. const existingIndex = newFilter.criteria.findIndex((c) => { @@ -463,22 +465,22 @@ const RenderList = < }); newFilter.currentPage = 1; - updateQueryParams(newFilter); + updateFilter(newFilter); setEditingCriterion(undefined); setNewCriterion(false); } function onRemoveCriterion(removedCriterion: Criterion) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.criteria = newFilter.criteria.filter( (criterion) => criterion.getId() !== removedCriterion.getId() ); newFilter.currentPage = 1; - updateQueryParams(newFilter); + updateFilter(newFilter); } function updateCriteria(c: Criterion[]) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.criteria = c.slice(); setNewCriterion(false); } @@ -490,9 +492,9 @@ const RenderList = < const content = (
- + setNewCriterion(true)} @@ -562,50 +564,52 @@ const useList = ( const history = useHistory(); const location = useLocation(); const [interfaceState, setInterfaceState] = useInterfaceLocalForage(); - // If persistState is false we don't care about forage and consider it initialised - const [forageInitialised, setForageInitialised] = useState( - !options.persistState - ); + const [filterInitialised, setFilterInitialised] = useState(false); // Store initial pathname to prevent hooks from operating outside this page const originalPathName = useRef(location.pathname); const persistanceKey = options.persistanceKey ?? options.filterMode; const defaultSort = options.defaultSort ?? filterOptions.defaultSortBy; const defaultDisplayMode = filterOptions.displayModeOptions[0]; - const [filter, setFilter] = useState( - new ListFilterModel( + const createNewFilter = useCallback(() => { + return new ListFilterModel( options.filterMode, - queryString.parse(location.search), + queryString.parse(history.location.search), defaultSort, defaultDisplayMode, options.defaultZoomIndex - ) - ); + ); + }, [ + options.filterMode, + history, + defaultSort, + defaultDisplayMode, + options.defaultZoomIndex, + ]); + const [filter, setFilter] = useState(createNewFilter); - const updateInterfaceConfig = useCallback( - (updatedFilter: ListFilterModel, level: PersistanceLevel) => { - if (level === PersistanceLevel.VIEW) { - setInterfaceState((prevState) => { - if (!prevState.queryConfig) { - prevState.queryConfig = {}; - } - return { - ...prevState, - queryConfig: { - ...prevState.queryConfig, - [persistanceKey]: { - ...prevState.queryConfig[persistanceKey], - filter: queryString.stringify({ - ...queryString.parse( - prevState.queryConfig[persistanceKey]?.filter ?? "" - ), - disp: updatedFilter.displayMode, - }), - }, + const updateSavedFilter = useCallback( + (updatedFilter: ListFilterModel) => { + setInterfaceState((prevState) => { + if (!prevState.queryConfig) { + prevState.queryConfig = {}; + } + return { + ...prevState, + queryConfig: { + ...prevState.queryConfig, + [persistanceKey]: { + ...prevState.queryConfig[persistanceKey], + filter: queryString.stringify({ + ...queryString.parse( + prevState.queryConfig[persistanceKey]?.filter ?? "" + ), + disp: updatedFilter.displayMode, + }), }, - }; - }); - } + }, + }; + }); }, [persistanceKey, setInterfaceState] ); @@ -616,115 +620,122 @@ const useList = ( } = useFindDefaultFilter(options.filterMode); const updateQueryParams = useCallback( - (listFilter: ListFilterModel) => { - setFilter(listFilter); - const newLocation = { ...location }; - newLocation.search = listFilter.makeQueryParameters(); - history.replace(newLocation); - if (options.persistState) { - updateInterfaceConfig(listFilter, options.persistState); - } + (newFilter: ListFilterModel) => { + const newParams = newFilter.makeQueryParameters(); + history.replace({ ...history.location, search: newParams }); }, - [setFilter, history, location, options.persistState, updateInterfaceConfig] + [history] ); - useEffect(() => { - if ( - // defer processing this until forage is initialised and - // default filter is loaded - interfaceState.loading || - defaultFilterLoading || - // Only update query params on page the hook was mounted on - history.location.pathname !== originalPathName.current - ) - return; - - if (!forageInitialised) setForageInitialised(true); - - const newFilter = filter.clone(); - let update = false; - - // Compare constructed filter with current filter. - // If different it's the result of navigation, and we update the filter. - if ( - history.location.search && - history.location.search !== `?${filter.makeQueryParameters()}` - ) { - newFilter.configureFromQueryParameters( - queryString.parse(history.location.search) - ); - update = true; - } - - // if default query is set and no search params are set, then - // load the default query - // #1512 - use default query only if persistState is ALL - if ( - options.persistState === PersistanceLevel.ALL && - !location.search && - defaultFilter?.findDefaultFilter - ) { - newFilter.currentPage = 1; - try { - newFilter.configureFromQueryParameters( - JSON.parse(defaultFilter.findDefaultFilter.filter) - ); - } catch (err) { - console.log(err); - // ignore + const updateFilter = useCallback( + (newFilter: ListFilterModel) => { + setFilter(newFilter); + updateQueryParams(newFilter); + if (options.persistState === PersistanceLevel.VIEW) { + updateSavedFilter(newFilter); } - // #1507 - reset random seed when loaded - newFilter.randomSeed = -1; - update = true; - } + }, + [options.persistState, updateSavedFilter, updateQueryParams] + ); - // set the display type if persisted - const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey]; + // 'Startup' hook, initialises the filters + useEffect(() => { + // Only run once + if (filterInitialised) return; - if (options.persistState === PersistanceLevel.VIEW && storedQuery) { - const storedFilter = queryString.parse(storedQuery.filter); + let newFilter = filter.clone(); - if (storedFilter.disp !== undefined) { - const displayMode = Number.parseInt(storedFilter.disp as string, 10); - if (displayMode !== newFilter.displayMode) { + if (options.persistState === PersistanceLevel.ALL) { + // only set default filter if query params are empty + if (!history.location.search) { + // wait until default filter is loaded + if (defaultFilterLoading) return; + + if (defaultFilter?.findDefaultFilter) { + newFilter.currentPage = 1; + try { + newFilter.configureFromQueryParameters( + JSON.parse(defaultFilter.findDefaultFilter.filter) + ); + } catch (err) { + console.log(err); + // ignore + } + // #1507 - reset random seed when loaded + newFilter.randomSeed = -1; + } + } + } else if (options.persistState === PersistanceLevel.VIEW) { + // wait until forage is initialised + if (interfaceState.loading) return; + + const storedQuery = interfaceState.data?.queryConfig?.[persistanceKey]; + if (options.persistState === PersistanceLevel.VIEW && storedQuery) { + const storedFilter = queryString.parse(storedQuery.filter); + if (storedFilter.disp !== undefined) { + const displayMode = Number.parseInt(storedFilter.disp as string, 10); newFilter.displayMode = displayMode; - update = true; } } } + setFilter(newFilter); + updateQueryParams(newFilter); - if (update) { - updateQueryParams(newFilter); - } + setFilterInitialised(true); }, [ - defaultSort, - defaultDisplayMode, + filterInitialised, filter, - interfaceState, history, - location.search, + options.persistState, updateQueryParams, defaultFilter, defaultFilterLoading, + interfaceState, persistanceKey, - forageInitialised, - options.persistState, ]); + // This hook runs on every page location change (ie navigation), + // and updates the filter accordingly. + useEffect(() => { + if (!filterInitialised) return; + + // Only update on page the hook was mounted on + if (location.pathname !== originalPathName.current) { + return; + } + + // Re-init filters on empty new query params + if (!location.search) { + setFilter(createNewFilter); + setFilterInitialised(false); + return; + } + + setFilter((prevFilter) => { + let newFilter = prevFilter.clone(); + newFilter.configureFromQueryParameters( + queryString.parse(location.search) + ); + if (!isEqual(newFilter, prevFilter)) { + return newFilter; + } else { + return prevFilter; + } + }); + }, [filterInitialised, createNewFilter, location]); + const onChangePage = useCallback( (page: number) => { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.currentPage = page; - updateQueryParams(newFilter); + updateFilter(newFilter); window.scrollTo(0, 0); }, - [filter, updateQueryParams] + [filter, updateFilter] ); const renderFilter = useMemo(() => { - return !options.filterHook - ? filter - : options.filterHook(_.cloneDeep(filter)); + return !options.filterHook ? filter : options.filterHook(cloneDeep(filter)); }, [filter, options]); const { contentTemplate, onSelectChange } = RenderList({ @@ -732,10 +743,10 @@ const useList = ( filter: renderFilter, filterOptions, onChangePage, - updateQueryParams, + updateFilter, }); - const template = !forageInitialised ? ( + const template = !filterInitialised ? ( ) : ( <>{contentTemplate} diff --git a/ui/v2.5/src/hooks/LocalForage.ts b/ui/v2.5/src/hooks/LocalForage.ts index 5d47a4c50..b8e6aae9a 100644 --- a/ui/v2.5/src/hooks/LocalForage.ts +++ b/ui/v2.5/src/hooks/LocalForage.ts @@ -1,5 +1,5 @@ import localForage from "localforage"; -import _ from "lodash"; +import isEqual from "lodash-es/isEqual"; import React, { Dispatch, SetStateAction, useEffect } from "react"; import { ConfigImageLightboxInput } from "src/core/generated-graphql"; @@ -69,7 +69,7 @@ export function useLocalForage( }, [loading, key, defaultValue]); useEffect(() => { - if (!_.isEqual(Cache[key], data)) { + if (isEqual(Cache[key], data)) { Cache[key] = { ...Cache[key], ...data, diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 99565d469..1e122c07c 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -8,7 +8,7 @@ @import "src/components/List/styles.scss"; @import "src/components/Movies/styles.scss"; @import "src/components/Performers/styles.scss"; -@import "src/components/Recommendations/styles.scss"; +@import "src/components/FrontPage/styles.scss"; @import "src/components/Scenes/styles.scss"; @import "src/components/SceneDuplicateChecker/styles.scss"; @import "src/components/SceneFilenameParser/styles.scss"; @@ -800,6 +800,12 @@ div.dropdown-menu { } } +// workaround for dropdown button in button group +.btn-group > .dropdown:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + dl.details-list { display: grid; grid-column-gap: 10px; diff --git a/ui/v2.5/src/locales/da-DK.json b/ui/v2.5/src/locales/da-DK.json index 96c836bb9..79767eeb8 100644 --- a/ui/v2.5/src/locales/da-DK.json +++ b/ui/v2.5/src/locales/da-DK.json @@ -860,11 +860,6 @@ "queue": "Kø", "random": "Tilfældig", "rating": "Bedømmelse", - "recently_added_performers": "Senest Tilføjede Skuespillere", - "recently_added_studios": "Senest Tilføjede Studier", - "recently_released_galleries": "Senest Tilføjede Gallerier", - "recently_released_movies": "Senest Tilføjede Film", - "recently_released_scenes": "Senest Tilføjede Scener", "resolution": "Opløsning", "scene": "Scene", "sceneTagger": "Scenetagger", diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index 5f50c3bfe..98f0a74e0 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -7,7 +7,7 @@ "allow": "Erlauben", "allow_temporarily": "Vorübergehend erlauben", "apply": "Übernehmen", - "auto_tag": "Auto Tag", + "auto_tag": "Auto-Tag", "backup": "Backup", "browse_for_image": "Nach Bild suchen…", "cancel": "Abbrechen", @@ -23,15 +23,18 @@ "create_entity": "Erstelle {entityType}", "create_marker": "Erstelle Markierung", "created_entity": "{entity_type} erstellt: {entity_name}", + "customise": "Anpassen", "delete": "Löschen", "delete_entity": "Lösche {entityType}", "delete_file": "Lösche Datei", "delete_file_and_funscript": "Datei löschen (inkl. funscript)", "delete_generated_supporting_files": "Lösche generierte Hilfsdaten", + "delete_stashid": "StashID löschen", "disallow": "Nicht erlauben", "download": "Herunterladen", "download_backup": "Lade Backup herunter", "edit": "Bearbeiten", + "edit_entity": "Bearbeiten {entityType}", "export": "Exportieren…", "export_all": "Alle exportieren…", "find": "Suchen", @@ -45,15 +48,18 @@ "generate_thumb_from_current": "Erstelle Vorschaubild vom Gegenwärtigen", "hash_migration": "Hash Umwandlung", "hide": "Verstecke", + "hide_configuration": "Konfiguration ausblenden", "identify": "Identifizieren", "ignore": "Ignorieren", "import": "Importieren…", "import_from_file": "Importieren aus Datei", + "logout": "Ausloggen", "merge": "Zusammenführen", "merge_from": "Zusammenführen aus", "merge_into": "Zusammenführen in", "next_action": "Nächste", "not_running": "wird nicht ausgeführt", + "open_in_external_player": "In externem Player öffnen", "open_random": "Öffne Zufällig", "overwrite": "Überschreiben", "play_random": "Zufällige Wiedergabe", @@ -64,6 +70,7 @@ "reload_plugins": "Plugins neu laden", "reload_scrapers": "Scraper neu laden", "remove": "Entfernen", + "remove_from_gallery": "Aus Gallerie entfernen", "rename_gen_files": "Hilfsdaten umbenennen", "rescan": "Erneut scannen", "reshuffle": "Neu mischen", @@ -78,6 +85,7 @@ "scrape_with": "Scrape mit…", "search": "Suchen", "select_all": "Alle auswählen", + "select_entity": "{entityType} auswählen", "select_folders": "Ordner auswählen", "select_none": "Nichts auswählen", "selective_auto_tag": "Automatisch selektiv taggen", @@ -88,9 +96,12 @@ "set_front_image": "Vorderseite…", "set_image": "Bild festlegen…", "show": "Anzeigen", + "show_configuration": "Konfiguration anzeigen", "skip": "Überspringen", "stop": "Stopp", + "submit": "Einreichen", "submit_stash_box": "Zu Stash-Box übermitteln", + "submit_update": "Aktualisierung übermitteln", "tasks": { "clean_confirm_message": "Wollen Sie wirklich die Datenbank aufräumen? Dies wird alle Informationen und Hilfsdaten für Szenen und Galerien löschen, die nicht mehr auf dem Dateisystem vorhanden sind.", "dry_mode_selected": "Trockenmodus ausgewählt. Es findet keine Löschung der Daten statt, lediglich Protokollierung.", @@ -98,6 +109,7 @@ }, "temp_disable": "Vorübergehend deaktivieren…", "temp_enable": "Vorübergehend aktivieren…", + "unset": "Aufheben", "use_default": "Standard verwenden", "view_random": "Zeige Zufällige" }, @@ -111,6 +123,7 @@ "birth_year": "Geburtsjahr", "birthdate": "Geburtsdatum", "bitrate": "Bitrate", + "captions": "Untertitel", "career_length": "Karrierelänge", "component_tagger": { "config": { @@ -190,14 +203,19 @@ "dlna": { "allow_temp_ip": "Erlaube {tempIP}", "allowed_ip_addresses": "Erlaubte IP Adressen", + "allowed_ip_temporarily": "Temporär erlaubte IP", "default_ip_whitelist": "Standard IP Whitelist", "default_ip_whitelist_desc": "Standard IP Adressen, welche DLNA nutzen dürfen. Nutze {wildcard} um alle IP Adressen zu erlauben.", + "disabled_dlna_temporarily": "DLNA vorübergehend deaktiviert", + "disallowed_ip": "Unzulässige IP", "enabled_by_default": "Standardmäßig aktiviert", + "enabled_dlna_temporarily": "DLNA vorübergehend aktiviert", "network_interfaces": "Netzwerkoberflächen", "network_interfaces_desc": "Netzwerkoberflächen auf denen DLNA sichtbar ist. Eine leere Liste führt dazu, dass DLNA auf allen Oberflächen ausgeführt wird. Benötigt Neustart des DLNA nach Änderungen.", "recent_ip_addresses": "Letzte IP Adressen", "server_display_name": "Server Anzeigename", "server_display_name_desc": "Anzeigename des DLNA-Servers. Standardmäßig {server_name} bei leerem Feld.", + "successfully_cancelled_temporary_behaviour": "Erfolgreich temporäres Verhalten aufgehoben", "until_restart": "bis Neustart" }, "general": { @@ -265,6 +283,10 @@ "number_of_parallel_task_for_scan_generation_head": "Anzahl paralleler Tasks für Scan/Generierung", "parallel_scan_head": "Paralleler Scan/Generierung", "preview_generation": "Vorschau-Generierung", + "python_path": { + "description": "Ort der Python-Programmdatei. Wird für Script-Scraper und Plugins verwendet. Wenn leer, wird python aus der Umgebung aufgelöst", + "heading": "Python Pfad" + }, "scraper_user_agent": "Scraper-Benutzeragent", "scraper_user_agent_desc": "User-Agent-String, der während Scrape-HTTP-Anfragen verwendet wird", "scrapers_path": { @@ -430,10 +452,23 @@ "description": "Zeitversatz in Millisekunden für interaktive Skriptwiedergabe.", "heading": "Funscript Zeitversatz (ms)" }, + "handy_connection": { + "connect": "Verbinden", + "server_offset": { + "heading": "Server Kompensation" + }, + "status": { + "heading": "Handy Verbindungsstatus" + }, + "sync": "Synchronisieren" + }, "handy_connection_key": { "description": "Handy Verbindungsschlüssel für interaktive Szenen. Wenn dieser Schlüssel gesetzt wird, kann Stash aktuellen Szeneinformationen mit handyfeeling.com teilen", "heading": "Handy Verbindungsschlüssel" }, + "image_lightbox": { + "heading": "Bild-Lightbox" + }, "images": { "heading": "Bilder", "options": { @@ -489,7 +524,8 @@ "continue_playlist_default": { "description": "Nächste Szene in der Warteschlange spielen", "heading": "Standardmäßig die Wiedergabeliste fortsetzen" - } + }, + "show_scrubber": "Scrubber anzeigen" } }, "scene_wall": { @@ -499,9 +535,13 @@ "toggle_sound": "Sound einschalten" } }, + "scroll_attempts_before_change": { + "description": "Anzahl der Versuche, einen Bildlauf durchzuführen, bevor zum nächsten/vorherigen Element gewechselt wird. Gilt nur für den Bildlaufmodus Schwenkung Y.", + "heading": "Anzahl Scroll-Versuche vor Übergang" + }, "slideshow_delay": { "description": "Die Diashow ist in Galerien in der Wandansicht verfügbar", - "heading": "Verzögerung der Diashow" + "heading": "Verzögerung der Diashow (Sekunden)" }, "title": "Benutzeroberfläche" } @@ -602,6 +642,8 @@ "marker_screenshots_tooltip": "Statische JPG-Bilder für Markierungen, nur erforderlich, wenn der Vorschautyp auf Statisches Bild eingestellt ist.", "markers": "Vorschau für Markierungen", "markers_tooltip": "20-Sekunden-Videos, die zum angegebenen Zeitpunkt beginnen.", + "override_preview_generation_options": "Überschreibe Optionen zur Erstellung von Vorschauen", + "override_preview_generation_options_desc": "Überschreibe die Optionen zur Erstellung von Vorschauen für diesen Vorgang. Die Standardeinstellungen werden unter System -> \tVorschau-Generierung festgelegt.", "overwrite": "Vorhandene generierte Dateien überschreiben", "phash": "Perzeptuelle Hashes (zur Deduplizierung)", "preview_exclude_end_time_desc": "Schließen Sie die letzten x Sekunden von der Szenenvorschau aus. Dies kann ein Wert in Sekunden oder ein Prozentsatz (zB 2%) der gesamten Szenendauer sein.", @@ -653,6 +695,7 @@ "search_accuracy_label": "Suchgenauigkeit", "title": "Szenen-Duplikate" }, + "duplicated_phash": "Dopplung (phash)", "duration": "Dauer", "effect_filters": { "aspect": "Seitenverhältnis", @@ -675,7 +718,9 @@ "scale": "Skalieren", "warmth": "Wärme" }, + "empty_server": "Fügen Sie Ihrem Server einige Szenen hinzu, um Empfehlungen auf dieser Seite anzuzeigen.", "ethnicity": "Ethnizität", + "existing_value": "vorhandener Wert", "eye_color": "Augenfarbe", "fake_tits": "Brustvergrößerungen", "false": "Falsch", @@ -690,6 +735,12 @@ "filters": "Filter", "framerate": "Bildrate", "frames_per_second": "{value} Bilder pro Sekunde", + "front_page": { + "types": { + "premade_filter": "Vorgefertigte Filter", + "saved_filter": "Gespeicherte Filter" + } + }, "galleries": "Galerien", "gallery": "Galerie", "gallery_count": "Galerienanzahl", @@ -703,9 +754,19 @@ "TRANSGENDER_MALE": "Trans* männlich" }, "hair_color": "Haarfarbe", + "handy_connection_status": { + "connecting": "Verbindet", + "disconnected": "Getrennt", + "error": "Fehler bei der Verbindung zu Handy", + "missing": "Fehlt", + "ready": "Bereit", + "syncing": "Synchronisiert mit Server", + "uploading": "Skript wird hochgeladen" + }, "hasMarkers": "Hat Markierungen", "height": "Größe", "help": "Hilfe", + "ignore_auto_tag": "Auto-Tag ignorieren", "image": "Bild", "image_count": "Bilderanzahl", "images": "Bilder", @@ -760,15 +821,55 @@ "parent_tags": "Übergeordnete Tags", "part_of": "Übergeordnet von {parent}", "path": "Pfad", + "perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)", "performer": "Darsteller", "performerTags": "Darsteller-Tags", + "performer_age": "Alter der Darsteller", "performer_count": "Darstelleranzahl", + "performer_favorite": "Darsteller favorisiert", "performer_image": "Darsteller-Bild", + "performer_tagger": { + "add_new_performers": "Neue Darsteller hinzufügen", + "any_names_entered_will_be_queried": "Alle eingetragenen Namen werden bei der stash-box Instanz nachgeschlagen und hinzugefügt, wenn gefunden. Nur exakte Übereinstimmungen werden als Treffer gewertet.", + "batch_add_performers": "Stapelverarbeitung für Darsteller", + "batch_update_performers": "Stapelverarbeitungsaktualisierung für Darsteller", + "config": { + "active_stash-box_instance": "Ausgewählte stash-box Instanz:", + "edit_excluded_fields": "Ausgeschlossene Felder bearbeiten", + "excluded_fields": "Ausgeschlossene Felder:", + "no_fields_are_excluded": "Keine Felder werden ausgeschlossen", + "no_instances_found": "Keine Instanzen gefunden", + "these_fields_will_not_be_changed_when_updating_performers": "Diese Felder werden durch die Aktualisierung nicht verändert." + }, + "current_page": "Aktuelle Seite", + "failed_to_save_performer": "Fehler beim Speichern der Darsteller \"{performer}\"", + "name_already_exists": "Name bereits vergeben", + "network_error": "Netzwerkfehler", + "no_results_found": "Keine Ergebnisse gefunden.", + "number_of_performers_will_be_processed": "{performer_count} Darsteller werden verarbeitet", + "performer_already_tagged": "Darsteller bereits getagged", + "performer_names_separated_by_comma": "Darstellernamen, mit Komma getrennt", + "performer_selection": "Darstellerauswahl", + "performer_successfully_tagged": "Darsteller erfolgreich getagged:", + "query_all_performers_in_the_database": "Alle Darsteller in der Datenbank", + "refresh_tagged_performers": "Aktualisieren getaggter Darsteller", + "refreshing_will_update_the_data": "Bei der Aktualisierung werden die Metadaten aller getaggten Darsteller über die stash-box-Instanz aktualisiert.", + "status_tagging_job_queued": "Status: Tagging-Auftrag in der Warteschlange", + "status_tagging_performers": "Status: Tagge Darsteller", + "tag_status": "Tag Status", + "to_use_the_performer_tagger": "Um den Darsteller-Tagger zu benutzen, muss eine stash-box Instanz konfiguriert sein.", + "untagged_performers": "Nicht getaggte Darsteller", + "update_performer": "Darsteller aktualisieren", + "update_performers": "Darsteller aktualisieren", + "updating_untagged_performers_description": "Bei der Aktualisierung von nicht getaggten Darstellern wird versucht die Metadaten alle Darsteller, welche keine StashID haben, zu aktualisieren." + }, "performers": "Darsteller", "piercings": "Piercings", "queue": "Playlist", "random": "Zufällig", "rating": "Wertung", + "recently_added_objects": "Kürzlich hinzugefügte {objects}", + "recently_released_objects": "Kürzlich erschienene {objects}", "resolution": "Auflösung", "scene": "Szene", "sceneTagger": "Szenen-Tagger", @@ -805,6 +906,10 @@ "something_went_wrong_description": "Es sieht so aus, als gäbe es Probleme mit deinen Eingaben, klicke Zurück und repariere sie. Falls du nicht weißt was du falsch gemacht hast, helfen wir gerne auf {discordLink}. Solltest du dir sicher sein einen Bug gefunden zu haben, schau doch mal auf {githubLink} vorbei.", "something_went_wrong_while_setting_up_your_system": "Etwas lief bei der Erstellung des Systems falsch. Hier ist die Fehlermeldung: {error}" }, + "folder": { + "file_path": "Dateipfad", + "up_dir": "Ein Verzeichnis hoch" + }, "github_repository": "Github Repository", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Backup Datenbank Pfad (Leer lassen, um Backups aus zu schalten):", @@ -858,13 +963,18 @@ "next_step": "Wenn du bereit bist ein neues System anzulegen, klicke Weiter.", "unable_to_locate_specified_config": "Wenn du das hier liest, konnte Stash die Konfigurationsdatei, welche spezifiziert wurde, nicht finden. Dieser Wizard wird dich deshalb durch den Prozess führen, eine neue Konfiguration anzulegen." }, - "welcome_to_stash": "Willkommen zu Stash", - "folder": { - "up_dir": "Ein Verzeichnis hoch" - } + "welcome_to_stash": "Willkommen zu Stash" }, "stash_id": "Stash-ID", "stash_ids": "Stash IDs", + "stashbox": { + "go_review_draft": "Gehe zu {endpoint_name}, um Entwurf zu begutachten.", + "selected_stash_box": "Ausgewählter Stash-Box Endpunkt", + "submission_failed": "Einreichen fehlgeschlagen", + "submission_successful": "Einreichen erfolgreich", + "submit_update": "Existiert bereits in {endpoint_name}" + }, + "statistics": "Statistiken", "stats": { "image_size": "Bildspeicher", "scenes_duration": "Szenendauer", @@ -903,9 +1013,11 @@ "total": "Gesamt", "true": "Wahr", "twitter": "Twitter", + "type": "Typ", "updated_at": "Aktualisiert am", "url": "URL", "videos": "Videos", + "view_all": "Alle ansehen", "weight": "Gewicht", "years_old": "Jahre alt" } diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 31ddca247..6b6a81894 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -18,15 +18,18 @@ "clear_image": "Clear Image", "close": "Close", "confirm": "Confirm", + "continue": "Continue", "create": "Create", "create_entity": "Create {entityType}", "create_marker": "Create Marker", "created_entity": "Created {entity_type}: {entity_name}", + "customise": "Customise", "delete": "Delete", "delete_entity": "Delete {entityType}", "delete_file": "Delete file", "delete_file_and_funscript": "Delete file (and funscript)", "delete_generated_supporting_files": "Delete generated supporting files", + "delete_stashid": "Delete StashID", "disallow": "Disallow", "download": "Download", "download_backup": "Download Backup", @@ -50,11 +53,13 @@ "ignore": "Ignore", "import": "Import…", "import_from_file": "Import from file", + "logout": "Log out", "merge": "Merge", "merge_from": "Merge from", "merge_into": "Merge into", "next_action": "Next", "not_running": "not running", + "open_in_external_player": "Open in external player", "open_random": "Open Random", "overwrite": "Overwrite", "play_random": "Play Random", @@ -65,6 +70,7 @@ "reload_plugins": "Reload plugins", "reload_scrapers": "Reload scrapers", "remove": "Remove", + "remove_from_gallery": "Remove from Gallery", "rename_gen_files": "Rename generated files", "rescan": "Rescan", "reshuffle": "Reshuffle", @@ -79,12 +85,12 @@ "scrape_with": "Scrape with…", "search": "Search", "select_all": "Select All", + "select_entity": "Select {entityType}", "select_folders": "Select folders", "select_none": "Select None", "selective_auto_tag": "Selective Auto Tag", "selective_clean": "Selective Clean", "selective_scan": "Selective Scan", - "select_entity": "Select {entityType}", "set_as_default": "Set as default", "set_back_image": "Back image…", "set_front_image": "Front image…", @@ -93,7 +99,9 @@ "show_configuration": "Show Configuration", "skip": "Skip", "stop": "Stop", + "submit": "Submit", "submit_stash_box": "Submit to Stash-Box", + "submit_update": "Submit update", "tasks": { "clean_confirm_message": "Are you sure you want to Clean? This will delete database information and generated content for all scenes and galleries that are no longer found in the filesystem.", "dry_mode_selected": "Dry Mode selected. No actual deleting will take place, only logging.", @@ -103,13 +111,7 @@ "temp_enable": "Enable temporarily…", "unset": "Unset", "use_default": "Use default", - "view_random": "View Random", - "continue": "Continue", - "submit": "Submit", - "logout": "Log out", - "remove_from_gallery": "Remove from Gallery", - "delete_stashid": "Delete StashID", - "open_in_external_player": "Open in external player" + "view_random": "View Random" }, "actions_name": "Actions", "age": "Age", @@ -201,20 +203,20 @@ "dlna": { "allow_temp_ip": "Allow {tempIP}", "allowed_ip_addresses": "Allowed IP addresses", + "allowed_ip_temporarily": "Allowed IP temporarily", "default_ip_whitelist": "Default IP Whitelist", "default_ip_whitelist_desc": "Default IP addresses allow to access DLNA. Use {wildcard} to allow all IP addresses.", + "disabled_dlna_temporarily": "Disabled DLNA temporarily", + "disallowed_ip": "Disallowed IP", "enabled_by_default": "Enabled by default", + "enabled_dlna_temporarily": "Enabled DLNA temporarily", "network_interfaces": "Interfaces", "network_interfaces_desc": "Interfaces to expose DLNA server on. An empty list results in running on all interfaces. Requires DLNA restart after changing.", "recent_ip_addresses": "Recent IP addresses", "server_display_name": "Server Display Name", "server_display_name_desc": "Display name for the DLNA server. Defaults to {server_name} if empty.", - "until_restart": "until restart", - "allowed_ip_temporarily": "Allowed IP temporarily", - "disabled_dlna_temporarily": "Disabled DLNA temporarily", - "disallowed_ip": "Disallowed IP", - "enabled_dlna_temporarily": "Enabled DLNA temporarily", - "successfully_cancelled_temporary_behaviour": "Successfully cancelled temporary behaviour" + "successfully_cancelled_temporary_behaviour": "Successfully cancelled temporary behaviour", + "until_restart": "until restart" }, "general": { "auth": { @@ -335,8 +337,8 @@ "tasks": { "added_job_to_queue": "Added {operation_name} to job queue", "auto_tag": { - "auto_tagging_paths": "Auto Tagging the following paths", - "auto_tagging_all_paths": "Auto Tagging all paths" + "auto_tagging_all_paths": "Auto Tagging all paths", + "auto_tagging_paths": "Auto Tagging the following paths" }, "auto_tag_based_on_filenames": "Auto-tag content based on filenames.", "auto_tagging": "Auto Tagging", @@ -350,8 +352,8 @@ "empty_queue": "No tasks are currently running.", "export_to_json": "Exports the database content into JSON format in the metadata directory.", "generate": { - "generating_scenes": "Generating for {num} {scene}", - "generating_from_paths": "Generating for scenes from the following paths" + "generating_from_paths": "Generating for scenes from the following paths", + "generating_scenes": "Generating for {num} {scene}" }, "generate_desc": "Generate supporting image, sprite, video, vtt and other files.", "generate_phashes_during_scan": "Generate perceptual hashes", @@ -392,8 +394,8 @@ "only_dry_run": "Only perform a dry run. Don't remove anything", "plugin_tasks": "Plugin Tasks", "scan": { - "scanning_paths": "Scanning the following paths", - "scanning_all_paths": "Scanning all paths" + "scanning_all_paths": "Scanning all paths", + "scanning_paths": "Scanning the following paths" }, "scan_for_content_desc": "Scan for new content and add it to the database.", "set_name_date_details_from_metadata_if_present": "Set name, date, details from embedded file metadata" @@ -434,10 +436,10 @@ }, "desktop_integration": { "desktop_integration": "Desktop Integration", - "skip_opening_browser": "Skip Opening Browser", - "skip_opening_browser_on_startup": "Skip auto-opening browser during startup", "notifications_enabled": "Enable Notifications", - "send_desktop_notifications_for_events": "Send desktop notifications for events" + "send_desktop_notifications_for_events": "Send desktop notifications for events", + "skip_opening_browser": "Skip Opening Browser", + "skip_opening_browser_on_startup": "Skip auto-opening browser during startup" }, "editing": { "disable_dropdown_create": { @@ -450,10 +452,6 @@ "description": "Time offset in milliseconds for interactive scripts playback.", "heading": "Funscript Offset (ms)" }, - "handy_connection_key": { - "description": "Handy connection key to use for interactive scenes. Setting this key will allow Stash to share your current scene information with handyfeeling.com", - "heading": "Handy Connection Key" - }, "handy_connection": { "connect": "Connect", "server_offset": { @@ -464,6 +462,13 @@ }, "sync": "Sync" }, + "handy_connection_key": { + "description": "Handy connection key to use for interactive scenes. Setting this key will allow Stash to share your current scene information with handyfeeling.com", + "heading": "Handy Connection Key" + }, + "image_lightbox": { + "heading": "Image Lightbox" + }, "images": { "heading": "Images", "options": { @@ -473,9 +478,6 @@ } } }, - "image_lightbox": { - "heading": "Image Lightbox" - }, "interactive_options": "Interactive Options", "language": { "heading": "Language" @@ -733,17 +735,23 @@ "filters": "Filters", "framerate": "Frame Rate", "frames_per_second": "{value} frames per second", + "front_page": { + "types": { + "premade_filter": "Premade Filter", + "saved_filter": "Saved Filter" + } + }, "galleries": "Galleries", "gallery": "Gallery", "gallery_count": "Gallery Count", "gender": "Gender", "gender_types": { - "MALE": "Male", "FEMALE": "Female", - "TRANSGENDER_MALE": "Transgender Male", - "TRANSGENDER_FEMALE": "Transgender Female", "INTERSEX": "Intersex", - "NON_BINARY": "Non-Binary" + "MALE": "Male", + "NON_BINARY": "Non-Binary", + "TRANSGENDER_FEMALE": "Transgender Female", + "TRANSGENDER_MALE": "Transgender Male" }, "hair_color": "Hair Colour", "handy_connection_status": { @@ -816,20 +824,52 @@ "perceptual_similarity": "Perceptual Similarity (phash)", "performer": "Performer", "performerTags": "Performer Tags", + "performer_age": "Performer Age", "performer_count": "Performer Count", "performer_favorite": "Performer Favourited", - "performer_age": "Performer Age", "performer_image": "Performer Image", + "performer_tagger": { + "add_new_performers": "Add New Performers", + "any_names_entered_will_be_queried": "Any names entered will be queried from the remote Stash-Box instance and added if found. Only exact matches will be considered a match.", + "batch_add_performers": "Batch Add Performers", + "batch_update_performers": "Batch Update Performers", + "config": { + "active_stash-box_instance": "Active stash-box instance:", + "edit_excluded_fields": "Edit Excluded Fields", + "excluded_fields": "Excluded fields:", + "no_fields_are_excluded": "No fields are excluded", + "no_instances_found": "No instances found", + "these_fields_will_not_be_changed_when_updating_performers": "These fields will not be changed when updating performers." + }, + "current_page": "Current page", + "failed_to_save_performer": "Failed to save performer \"{performer}\"", + "name_already_exists": "Name already exists", + "network_error": "Network Error", + "no_results_found": "No results found.", + "number_of_performers_will_be_processed": "{performer_count} performers will be processed", + "performer_already_tagged": "Performer already tagged", + "performer_names_separated_by_comma": "Performer names separated by comma", + "performer_selection": "Performer selection", + "performer_successfully_tagged": "Performer successfully tagged:", + "query_all_performers_in_the_database": "All performers in the database", + "refresh_tagged_performers": "Refresh tagged performers", + "refreshing_will_update_the_data": "Refreshing will update the data of any tagged performers from the stash-box instance.", + "status_tagging_job_queued": "Status: Tagging job queued", + "status_tagging_performers": "Status: Tagging performers", + "tag_status": "Tag Status", + "to_use_the_performer_tagger": "To use the performer tagger a stash-box instance needs to be configured.", + "untagged_performers": "Untagged performers", + "update_performer": "Update Performer", + "update_performers": "Update Performers", + "updating_untagged_performers_description": "Updating untagged performers will try to match any performers that lack a stashid and update the metadata." + }, "performers": "Performers", "piercings": "Piercings", "queue": "Queue", "random": "Random", "rating": "Rating", - "recently_added_performers": "Recently Added Performers", - "recently_added_studios": "Recently Added Studios", - "recently_released_galleries": "Recently Released Galleries", - "recently_released_movies": "Recently Released Movies", - "recently_released_scenes": "Recently Released Scenes", + "recently_added_objects": "Recently Added {objects}", + "recently_released_objects": "Recently Released {objects}", "resolution": "Resolution", "scene": "Scene", "sceneTagger": "Scene Tagger", @@ -866,6 +906,10 @@ "something_went_wrong_description": "If this looks like a problem with your inputs, go ahead and click back to fix them up. Otherwise, raise a bug on the {githubLink} or seek help in the {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Something went wrong while setting up your system. Here is the error we received: {error}" }, + "folder": { + "file_path": "File path", + "up_dir": "Up a directory" + }, "github_repository": "Github repository", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Backup database path (leave empty to disable backup):", @@ -919,14 +963,17 @@ "next_step": "When you're ready to proceed with setting up a new system, click Next.", "unable_to_locate_specified_config": "If you're reading this, then Stash couldn't find the configuration file specified at the command line or the environment. This wizard will guide you through the process of setting up a new configuration." }, - "welcome_to_stash": "Welcome to Stash", - "folder": { - "file_path": "File path", - "up_dir": "Up a directory" - } + "welcome_to_stash": "Welcome to Stash" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", + "stashbox": { + "go_review_draft": "Go to {endpoint_name} to review draft.", + "selected_stash_box": "Selected Stash-Box endpoint", + "submission_failed": "Submission failed", + "submission_successful": "Submission successful", + "submit_update": "Already exists in {endpoint_name}" + }, "statistics": "Statistics", "stats": { "image_size": "Images size", @@ -950,6 +997,7 @@ "toast": { "added_entity": "Added {entity}", "added_generation_job_to_queue": "Added generation job to queue", + "created_entity": "Created {entity}", "default_filter_set": "Default filter set", "delete_entity": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}", @@ -958,59 +1006,18 @@ "rescanning_entity": "Rescanning {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "Saved {entity}", "started_auto_tagging": "Started auto tagging", - "updated_entity": "Updated {entity}", "started_generating": "Started generating", "started_importing": "Started importing", - "created_entity": "Created {entity}" + "updated_entity": "Updated {entity}" }, "total": "Total", "true": "True", "twitter": "Twitter", + "type": "Type", "updated_at": "Updated At", "url": "URL", "videos": "Videos", "view_all": "View All", "weight": "Weight", - "years_old": "years old", - "stashbox": { - "selected_stash_box": "Selected Stash-Box endpoint", - "submission_successful": "Submission successful", - "submission_failed": "Submission failed", - "go_review_draft": "Go to {endpoint_name} to review draft." - }, - "performer_tagger": { - "network_error": "Network Error", - "failed_to_save_performer": "Failed to save performer \"{performer}\"", - "name_already_exists": "Name already exists", - "performer_already_tagged": "Performer already tagged", - "current_page": "Current page", - "performer_successfully_tagged": "Performer successfully tagged:", - "no_results_found": "No results found.", - "update_performer": "Update Performer", - "update_performers": "Update Performers", - "query_all_performers_in_the_database": "All performers in the database", - "tag_status": "Tag Status", - "untagged_performers": "Untagged performers", - "updating_untagged_performers_description": "Updating untagged performers will try to match any performers that lack a stashid and update the metadata.", - "refresh_tagged_performers": "Refresh tagged performers", - "refreshing_will_update_the_data": "Refreshing will update the data of any tagged performers from the stash-box instance.", - "add_new_performers": "Add New Performers", - "to_use_the_performer_tagger": "To use the performer tagger a stash-box instance needs to be configured.", - "performer_selection": "Performer selection", - "performer_names_separated_by_comma": "Performer names separated by comma", - "any_names_entered_will_be_queried": "Any names entered will be queried from the remote Stash-Box instance and added if found. Only exact matches will be considered a match.", - "batch_add_performers": "Batch Add Performers", - "batch_update_performers": "Batch Update Performers", - "status_tagging_performers": "Status: Tagging performers", - "status_tagging_job_queued": "Status: Tagging job queued", - "number_of_performers_will_be_processed": "{performer_count} performers will be processed", - "config": { - "excluded_fields": "Excluded fields:", - "these_fields_will_not_be_changed_when_updating_performers": "These fields will not be changed when updating performers.", - "edit_excluded_fields": "Edit Excluded Fields", - "no_fields_are_excluded": "No fields are excluded", - "active_stash-box_instance": "Active stash-box instance:", - "no_instances_found": "No instances found" - } - } + "years_old": "years old" } diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index 9146f03f7..7596b7b08 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -23,6 +23,7 @@ "create_entity": "Crear {entityType}", "create_marker": "Crear marcador", "created_entity": "{entity_type} creado: {entity_name}", + "customise": "Personalizar", "delete": "Eliminar", "delete_entity": "Eliminar {entityType}", "delete_file": "Eliminar archivo", @@ -100,6 +101,7 @@ "stop": "Parar", "submit": "Enviar", "submit_stash_box": "Enviar a Stash-Box", + "submit_update": "Enviar Actualización", "tasks": { "clean_confirm_message": "¿Estás seguro que quieres iniciar la limpieza? Esto eliminará la información en la base de datos, y el contenido generado para todas las escenas y galerías que ya no estén disponibles en el sistema de ficheros.", "dry_mode_selected": "Modo de simulación seleccionado. No se eliminará información, solo se guardarán registros de las acciones a realizar.", @@ -121,6 +123,7 @@ "birth_year": "Año de nacimiento", "birthdate": "Cumpleaños", "bitrate": "Tasa de bits", + "captions": "Subtítulos", "career_length": "Años en activo", "component_tagger": { "config": { @@ -449,6 +452,10 @@ "description": "Tiempo de compensación en milisegundos para la reproducción de scripts interactivos.", "heading": "Tiempo de compensación Funscript (mseg)" }, + "handy_connection": { + "connect": "Conectar", + "sync": "Sincronizar" + }, "handy_connection_key": { "description": "Clave para conexión práctica que se usará en las escenas interactivas. Configurar esta clave permitirá a Stash compartir la información actual de las escenas con handyfeeling.com", "heading": "Clave para conexión práctica" @@ -730,6 +737,13 @@ "TRANSGENDER_MALE": "Varón transgénero" }, "hair_color": "Color de pelo", + "handy_connection_status": { + "connecting": "Conectando", + "disconnected": "Desconectado", + "missing": "El archivo no esta disponsible", + "ready": "Conexion Preparada", + "syncing": "Sincronizando con el servidor" + }, "hasMarkers": "Tiene marcadores", "height": "Estatura", "help": "Ayuda", diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index 811094440..38b042d87 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -28,10 +28,12 @@ "delete_file": "Poista tiedosto", "delete_file_and_funscript": "Poista tiedosto (ja funscript)", "delete_generated_supporting_files": "Poista generoidut lisätiedostot", + "delete_stashid": "Poista StashID", "disallow": "Kiellä", "download": "Lataa", "download_backup": "Lataa varmuuskopio", "edit": "Muokkaa", + "edit_entity": "Muokkaa {entityType}", "export": "Vie…", "export_all": "Vie kaikki…", "find": "Etsi", @@ -50,11 +52,13 @@ "ignore": "Jätä huomiotta", "import": "Tuo…", "import_from_file": "Tuo tiedostosta", + "logout": "Kirjaudu ulos", "merge": "Yhdistä", "merge_from": "Yhdistä kohteesta", "merge_into": "Yhdistä kohteeseen", "next_action": "Seuraava", "not_running": "ei käynnissä", + "open_in_external_player": "Avaa ulkoisessa soittimessa", "open_random": "Avaa satunnainen", "overwrite": "Ylikirjoita", "play_random": "Toista satunnainen", @@ -65,6 +69,7 @@ "reload_plugins": "Lataa lisäosat uudelleen", "reload_scrapers": "Lataa kaapija uudelleen", "remove": "Poista", + "remove_from_gallery": "Poista galleriasta", "rename_gen_files": "Nimeä generoidut tiedostot uudelleen", "rescan": "Skannaa uudelleen", "reshuffle": "Sekoita uudelleen", @@ -79,6 +84,7 @@ "scrape_with": "Kaavi…", "search": "Hae", "select_all": "Valitse Kaikki", + "select_entity": "Valitse {entityType}", "select_folders": "Valitse kansiot", "select_none": "Peruuta Valinta", "selective_auto_tag": "Valikoiva automaattinen tunnisteiden asetus", @@ -94,6 +100,7 @@ "stop": "Pysäytä", "submit": "Lähetä", "submit_stash_box": "Lähetä Stash-Boxiin", + "submit_update": "Lähetä päivitys", "tasks": { "clean_confirm_message": "Haluatko varmasti puhdistaa? Tämä poistaa tietokannan tiedot ja poistaa kaikki generoidut tukitiedostot kaikista kohtauksista ja gallerioista, eikä niitä enää ole löydettävissä levyltä.", "dry_mode_selected": "Kuivatila käytössä. Poistoa ei oikeasti tehdä, vain lokikirjaus.", @@ -114,6 +121,7 @@ "birth_year": "Syntymävuosi", "birthdate": "Syntymäpäivä", "bitrate": "Bittinopeus", + "captions": "Tekstitykset", "career_length": "Uran pituus", "component_tagger": { "config": { @@ -193,9 +201,13 @@ "dlna": { "allow_temp_ip": "Salli {tempIP}", "allowed_ip_addresses": "Sallitut IP -osoitteet", + "allowed_ip_temporarily": "Väliaikaisesti sallittu IP", "default_ip_whitelist": "Oletus IP Whitelist", "default_ip_whitelist_desc": "Oletus IP -osoitteet, joilla on DLNA -pääsy. Käytä {wildcard} salliaksesi kaikki IP -osoitteet.", + "disabled_dlna_temporarily": "DLNA poistettu käytöstä väliaikaisesti", + "disallowed_ip": "Estetty IP", "enabled_by_default": "Sallittu oletuksena", + "enabled_dlna_temporarily": "DNLA päällä väliaikaisesti", "network_interfaces": "Rajapinnat", "network_interfaces_desc": "Rajapinnat, joille DLNA palvelin näytetään. Tyhjä lista sallii kaikki rajapinnat. Vaatii DLNA -palvelimen uudelleenkäynnistyksen, mikäli asetusta muokataan.", "recent_ip_addresses": "Viimeisimmät IP -osoitteet", @@ -268,6 +280,9 @@ "number_of_parallel_task_for_scan_generation_head": "Rinnakkaisten skannaus- ja generointitehtävien määrä", "parallel_scan_head": "Rinnakkainen skannaus ja generointi", "preview_generation": "Esikatselun generointi", + "python_path": { + "heading": "Pythonin polku" + }, "scraper_user_agent": "Kaapijan käyttäjäagentti", "scraper_user_agent_desc": "Käyttäjäagenttikenttä, jota kaavittaessa käytetään http pyynnöissä", "scrapers_path": { @@ -422,6 +437,10 @@ "funscript_offset": { "description": "Viive millisekunneissa interaktiivisille skripteille kun toistetaan." }, + "handy_connection": { + "connect": "Yhdistä", + "sync": "Synkronoi" + }, "images": { "heading": "Kuvat", "options": { @@ -431,6 +450,7 @@ } } }, + "interactive_options": "Interaktiivisuuden asetukset", "language": { "heading": "Kieli" }, @@ -563,6 +583,7 @@ }, "scroll_mode": { "label": "Vieritystila", + "pan_y": "Pan Y", "zoom": "Zoomaus" } }, @@ -651,7 +672,9 @@ "scale": "Skaalaa", "warmth": "Lämpötila" }, + "empty_server": "Lisää kohtauksia palvelimeesi niin näet suosituksia tällä sivulla.", "ethnicity": "Etninen tausta", + "existing_value": "nykyinen arvo", "eye_color": "Silmien väri", "fake_tits": "Tekorinnat", "false": "Ei", @@ -679,6 +702,15 @@ "TRANSGENDER_MALE": "Transmies" }, "hair_color": "Hiusten väri", + "handy_connection_status": { + "connecting": "Yhdistetään", + "disconnected": "Ei yhdistetty", + "error": "Virhe yhdistettäessä Handyyn", + "missing": "Puuttuu", + "ready": "Valmis", + "syncing": "Synkronoidaan palvelimelle", + "uploading": "Ladataan skriptiä" + }, "hasMarkers": "On merkki", "height": "Pituus", "help": "Apua", @@ -736,10 +768,12 @@ "parent_tags": "Ylätunnisteet", "part_of": "Osa {parent}", "path": "Polku", + "perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)", "performer": "Esiintyjä", "performerTags": "Esiintyjien tunnisteet", "performer_age": "Esiintyjän ikä", "performer_count": "Esiintyjien määrä", + "performer_favorite": "Esiintyjä suosikeissa", "performer_image": "Esiintyjän kuva", "performer_tagger": { "add_new_performers": "Lisää uusia esiintyjiä", @@ -758,11 +792,14 @@ "network_error": "Verkkovirhe", "no_results_found": "Ei tuloksia.", "number_of_performers_will_be_processed": "{performer_count} esintyjää prosessoidaan", + "performer_already_tagged": "Esiintyjälle on jo asetettu tunnisteet", "performer_names_separated_by_comma": "Erota esiintyjien nimet pilkulla", "performer_selection": "Esiintyjän valinta", + "performer_successfully_tagged": "Esiintyjälle on asetettu tunnisteet:", "query_all_performers_in_the_database": "Kaikki esiintyjät tietokannassa", "status_tagging_job_queued": "Tila: Tunnisteiden asettaminen laitettu jonoon", "status_tagging_performers": "Tila: Asetetaan esiintyjien tunnisteita", + "untagged_performers": "Esiintyjät joille ei ole asetettu tunnisteita", "update_performer": "Päivitä esiintyjä", "update_performers": "Päivitä esiintyjät" }, @@ -807,6 +844,10 @@ "something_went_wrong_description": "Näyttää siltä, että olet syöttänyt jotain omituista. Palaa takaisin korjataksesi ne. Muussa tapauksessa tee ilmoitus bugista {githubLink} tai pyydä apua {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Järjestelmän asettamisessa meni jotain vikaan. Tässä on virhe jonka saimme: {error}" }, + "folder": { + "file_path": "Tiedostopolku", + "up_dir": "Ylös" + }, "github_repository": "Github repository", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Tietokannan varmuuskopion polku (jätä tyhjäksi jos et halua varmuuskopiointia):", @@ -851,18 +892,17 @@ "next_step": "Kun olet valmis etenemään järjestelmän luontiin, paina Seuraava.", "unable_to_locate_specified_config": "Mikäli luet tätä, Stash ei löydä konfiguraatiotiedostoa, joka on määritelty joko komentorivillä tai muualla. Tämä velho auttaa sinua uuden konfiguraation luomisessa." }, - "welcome_to_stash": "Tervetuloa Stashiin", - "folder": { - "up_dir": "Ylös" - } + "welcome_to_stash": "Tervetuloa Stashiin" }, "stash_id": "Stash ID", "stash_ids": "Stash ID:t", "stashbox": { "go_review_draft": "Mene {endpoint_name} katsoaksesi luonnosta.", "submission_failed": "Lähettäminen ei onnistunut", - "submission_successful": "Lähettäminen onnistui" + "submission_successful": "Lähettäminen onnistui", + "submit_update": "On jo kohteessa {endpoint_name}" }, + "statistics": "Tilastot", "stats": { "image_size": "Kuvien koko", "scenes_duration": "Kohtausten kesto", @@ -904,6 +944,7 @@ "updated_at": "Päivitetty", "url": "URL", "videos": "Videot", + "view_all": "Näytä kaikki", "weight": "Paino", "years_old": "-vuotias" } diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 9a42cfd69..52e622b14 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -7,96 +7,109 @@ "allow": "Autoriser", "allow_temporarily": "Autoriser temporairement", "apply": "Appliquer", - "auto_tag": "Taggage automatique", + "auto_tag": "Étiquetage auto", "backup": "Sauvegarde", "browse_for_image": "Sélectionner une image…", "cancel": "Annuler", "clean": "Nettoyer", - "clear": "Vider", - "clear_back_image": "Suppr. l'image Verso", - "clear_front_image": "Suppr. l'image Recto", - "clear_image": "Supprimer l'image", + "clear": "Effacer", + "clear_back_image": "Effacer l'image verso", + "clear_front_image": "Effacer l'image recto", + "clear_image": "Effacer l'image", "close": "Fermer", "confirm": "Confirmer", "continue": "Continuer", "create": "Créer", "create_entity": "Créer {entityType}", - "create_marker": "Créer un Marqueur", - "created_entity": "Créé : {entity_type} : {entity_name}", + "create_marker": "Créer un marqueur", + "created_entity": "Créé {entity_type} : {entity_name}", + "customise": "Personnaliser", "delete": "Supprimer", "delete_entity": "Supprimer {entityType}", "delete_file": "Supprimer le fichier", - "delete_generated_supporting_files": "Supprimer les fichiers générés", + "delete_file_and_funscript": "Supprimer le fichier (et funscript)", + "delete_generated_supporting_files": "Supprimer les fichiers générés associés", + "delete_stashid": "Supprimer StashID", "disallow": "Refuser", "download": "Télécharger", "download_backup": "Télécharger une sauvegarde", - "edit": "Editer", + "edit": "Éditer", + "edit_entity": "Éditer {entityType}", "export": "Exporter…", "export_all": "Exporter tout…", - "find": "Trouver", + "find": "Rechercher", "finish": "Terminer", "from_file": "A partir du fichier…", "from_url": "A partir de l'URL…", - "full_export": "Export Complet", - "full_import": "Import Complet", + "full_export": "Export complet", + "full_import": "Import complet", "generate": "Générer", - "generate_thumb_default": "Générer la miniature par défaut", - "generate_thumb_from_current": "Générer une miniature", + "generate_thumb_default": "Générer une vignette par défaut", + "generate_thumb_from_current": "Générer une vignette à partir de l'image courante", "hash_migration": "Migration du hash", "hide": "Masquer", + "hide_configuration": "Masquer la configuration", "identify": "Identifier", "ignore": "Ignorer", "import": "Importer…", - "import_from_file": "Importer à partir d'un fichier", + "import_from_file": "Importation depuis un fichier", + "logout": "Déconnecter", "merge": "Fusionner", "merge_from": "Fusionner depuis", "merge_into": "Fusionner dans", "next_action": "Suivant", "not_running": "pas en cours d'exécution", + "open_in_external_player": "Ouvrir dans un lecteur externe", "open_random": "Ouvrir au hasard", - "overwrite": "Ecraser", + "overwrite": "Écraser", "play_random": "Lecture aléatoire", "play_selected": "Lire la sélection", "preview": "Aperçu", "previous_action": "Précédent", "refresh": "Rafraichir", - "reload_plugins": "Recharger les Plugins", - "reload_scrapers": "Recharger les Scrapers", + "reload_plugins": "Recharger les plugins", + "reload_scrapers": "Recharger les scrapers", "remove": "Retirer", + "remove_from_gallery": "Supprimer de la galerie", "rename_gen_files": "Renommer les fichiers générés", - "rescan": "Scanner à nouveau", + "rescan": "Analyser à nouveau", "reshuffle": "Mélanger à nouveau", "running": "en cours d'exécution", "save": "Sauvegarder", - "save_delete_settings": "Utilisez ces options par défaut lors de la suppression", + "save_delete_settings": "Utiliser ces options par défaut lors de la suppression", "save_filter": "Sauvegarder le filtre", - "scan": "Scanner", - "scrape": "Scrape", - "scrape_query": "Requête de Scrape", - "scrape_scene_fragment": "Scrape par fragment", - "scrape_with": "Scrape avec…", - "search": "Chercher", + "scan": "Analyser", + "scrape": "Scraper", + "scrape_query": "Requête Scrape", + "scrape_scene_fragment": "Scraper par fragment", + "scrape_with": "Scraper avec…", + "search": "Recherche", "select_all": "Sélectionner tout", + "select_entity": "Sélectionner {entityType}", "select_folders": "Sélectionner des répertoires", "select_none": "Ne rien sélectionner", - "selective_auto_tag": "Taggage automatique de la sélection", + "selective_auto_tag": "Étiquetage auto de la sélection", "selective_clean": "Nettoyage sélectif", - "selective_scan": "Scan sélectif", - "set_as_default": "Définir comme valeur par défaut", - "set_back_image": "Image Verso…", - "set_front_image": "Image Recto…", - "set_image": "Choisir l'image…", + "selective_scan": "Analyse sélective", + "set_as_default": "Définir par défaut", + "set_back_image": "Image verso…", + "set_front_image": "Image recto…", + "set_image": "Définir l'image…", "show": "Montrer", + "show_configuration": "Afficher la configuration", "skip": "Passer", "stop": "Stop", + "submit": "Soumettre", + "submit_stash_box": "Soumettre à Stash-Box", "tasks": { - "clean_confirm_message": "Êtes-vous sûr de vouloir nettoyer ? Cela supprimera les informations de la base de données et le contenu généré pour toutes les Vidéos et Galeries qui ne se trouvent plus dans le système de fichiers.", - "dry_mode_selected": "Essais à blanc. Aucune suppression n'aura lieu.", - "import_warning": "Êtes-vous sûr de vouloir importer ? Cela supprimera la base de données et réimportera des données à partir de vos métadonnées exportées." + "clean_confirm_message": "Êtes-vous sûr de vouloir nettoyer ? Cette opération supprimera les informations de la base de données et le contenu généré pour toutes les scènes et galeries qui ne se trouvent plus dans le système de fichiers.", + "dry_mode_selected": "Essais à blanc. Aucune suppression réelle n'aura lieu, seulement une journalisation.", + "import_warning": "Êtes-vous sûr de vouloir importer ? Cela supprimera la base de données et la réimportera à partir de vos métadonnées exportées." }, "temp_disable": "Désactiver temporairement…", "temp_enable": "Activer temporairement…", - "use_default": "Utiliser la valeur par défaut", + "unset": "Désactiver", + "use_default": "Utiliser par défaut", "view_random": "Visionner au hasard" }, "actions_name": "Actions", @@ -108,58 +121,59 @@ "average_resolution": "Résolution moyenne", "birth_year": "Année de naissance", "birthdate": "Date de naissance", - "bitrate": "BitRate", + "bitrate": "Débit", + "captions": "Légendes", "career_length": "Durée de la carrière", "component_tagger": { "config": { - "active_instance": "Instance stash-box active :", - "blacklist_desc": "Éléments à exclure de la requête, sous la forme d'une expression régulière insensible à la casse. Certains caractères doivent être échapés avec un backslash : {chars_require_escape}", - "blacklist_label": "Exclure", + "active_instance": "Instance Stash-Box active :", + "blacklist_desc": "Les éléments de la liste noire sont exclus des requêtes. Notez que ce sont des expressions régulières insensibles à la casse. Certains caractères doivent être échappés par une barre oblique inversée : {chars_require_escape}", + "blacklist_label": "Liste noire", "query_mode_auto": "Automatique", - "query_mode_auto_desc": "Se base sur les métadonnées du fichier si présentes, sinon utilise le nom du fichier", + "query_mode_auto_desc": "Utilise les métadonnées si présentes, ou le nom de fichier", "query_mode_dir": "Répertoire parent", - "query_mode_dir_desc": "Se base uniquement sur le nom du répertoire parent", + "query_mode_dir_desc": "Utilise uniquement le répertoire parent du fichier vidéo", "query_mode_filename": "Nom de fichier", - "query_mode_filename_desc": "Se base uniquement sur le nom du fichier", + "query_mode_filename_desc": "Utilise uniquement le nom du fichier", "query_mode_label": "Mode", "query_mode_metadata": "Métadonnées", - "query_mode_metadata_desc": "Se base uniquement sur les métadonnées du fichier", - "query_mode_path": "Chemin complet", - "query_mode_path_desc": "Se base sur le chemin complet du fichier", - "set_cover_desc": "Si une vignette a été trouvée, remplace la vignette de la vidéo par la vignette trouvée.", - "set_cover_label": "Enregistrer la vignette", - "set_tag_desc": "Ajoute les Tags à la vidéo, en fusionnant avec ou en écrasant les Tags existants.", - "set_tag_label": "Enregistrer les Tags", - "show_male_desc": "Cochez si vous voulez Taguer également les Acteurs (hommes).", - "show_male_label": "Montrer également les Acteurs", + "query_mode_metadata_desc": "Utilise uniquement les métadonnées", + "query_mode_path": "Chemin", + "query_mode_path_desc": "Utilise le chemin complet du fichier", + "set_cover_desc": "Remplace la couverture de la scène si une est trouvée.", + "set_cover_label": "Définir l'image de couverture de la scène", + "set_tag_desc": "Attache des étiquettes à la scène, en écrasant ou en fusionnant avec des étiquettes existantes.", + "set_tag_label": "Définir les étiquettes", + "show_male_desc": "Cocher si les performeurs masculins seront disponibles pour le marquage.", + "show_male_label": "Montrer les performeurs masculins", "source": "Source" }, "noun_query": "Requête", "results": { - "duration_off": "La durée diffère d'au moins {number} secondes", + "duration_off": "Durée différente d'au moins {number}s", "duration_unknown": "Durée inconnue", - "fp_found": "{fpCount, plural, =0 {Aucune empreinte correspondante} other {# nouvelles empreintes correspondantes}}", + "fp_found": "{fpCount, plural, =0 {Aucune nouvelle correspondance d'empreinte trouvée} other {# nouvelles correspondances d'empreintes trouvées}}", "fp_matches": "La durée correspond", - "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreintes(s)", - "hash_matches": "Correspondance trouvée : {hash_type}", - "match_failed_already_tagged": "Vidéo déjà taguée", + "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreinte(s)", + "hash_matches": "{hash_type} est une correspondance", + "match_failed_already_tagged": "Scène déjà étiquetée", "match_failed_no_result": "Aucun résultat trouvé", - "match_success": "Scène Taguée avec succès", - "phash_matches": "{count} PHashe(s) correspondant(s)", - "unnamed": "Anonyme" + "match_success": "Scène étiquetée avec succès", + "phash_matches": "{count} PHashes correspondants", + "unnamed": "Sans nom" }, "verb_match_fp": "Empreintes correspondantes", "verb_matched": "Associé", "verb_scrape_all": "Extraire tout", "verb_submit_fp": "Soumettre {fpCount, plural, one{# Empreinte} other{# Empreintes}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} vidéos associées" + "verb_toggle_unmatched": "{toggle} scènes incomparables" }, "config": { "about": { - "build_hash": "Hash de la version installée :", - "build_time": "Date de la version installée :", - "check_for_new_version": "Rechercher une mise à jour", + "build_hash": "Hash de construction :", + "build_time": "Date de construction :", + "check_for_new_version": "Rechercher une nouvelle version", "latest_version": "Dernière version", "latest_version_build_hash": "Dernière version de hachage :", "new_version_notice": "[Nouveautés]", @@ -188,20 +202,25 @@ "dlna": { "allow_temp_ip": "Autoriser {tempIP}", "allowed_ip_addresses": "Adresses IP autorisées", + "allowed_ip_temporarily": "IP autorisée temporairement", "default_ip_whitelist": "Liste blanche d'adresses IP", - "default_ip_whitelist_desc": "Liste d'adresses IP que seront autorisées par défaut à accéder au serveur DLNA. Utilisez {wildcard} pour autoriser toutes les adresses.", - "enabled_by_default": "Activer par défaut", + "default_ip_whitelist_desc": "Adresses IP par défaut autorisées pour accéder à DLNA. Utiliser {wildcard} pour autoriser toutes les adresses IP.", + "disabled_dlna_temporarily": "Désactivation temporaire de DLNA", + "disallowed_ip": "IP non autorisé", + "enabled_by_default": "Activé par défaut", + "enabled_dlna_temporarily": "Activation temporaire de DLNA", "network_interfaces": "Interfaces", - "network_interfaces_desc": "Interfaces réseaux sur lesquelles exposer le serveur DLNA. Si rien n'est spécifié, le serveur sera exposé sur toutes les interfaces disponibles. Redémarrez le serveur DLNA pour appliquer la modification.", + "network_interfaces_desc": "Interfaces sur lesquelles exposer le serveur DLNA. Une liste vide entraîne l'exécution sur toutes les interfaces. Nécessite le redémarrage de DLNA après modification.", "recent_ip_addresses": "Adresses IP récentes", - "server_display_name": "Nom du serveur DLNA", - "server_display_name_desc": "Nom du serveur DLNA. Si aucun nom n'est spécifié, le nom du serveur sera {server_name}.", + "server_display_name": "Nom d'affichage du serveur", + "server_display_name_desc": "Nom d'affichage du serveur DLNA. Par défaut {server_name} si vide.", + "successfully_cancelled_temporary_behaviour": "Le comportement temporaire a été annulé avec succès", "until_restart": "jusqu'au redémarrage" }, "general": { "auth": { "api_key": "Clé API", - "api_key_desc": "Clé API pour système tier. Requis uniquement lorsque un nom d'utilisateur et un mot de passe ont été spécifiés. Le nom d'utilisateur doit avoir été enregistré avant de générer la clé API.", + "api_key_desc": "Clé API pour les systèmes externes. Nécessaire uniquement lorsque le nom d'utilisateur/mot de passe est configuré. Le nom d'utilisateur doit être enregistré avant de générer la clé API.", "authentication": "Authentification", "clear_api_key": "Effacer la clé API", "credentials": { @@ -210,68 +229,73 @@ }, "generate_api_key": "Générer une clé API", "log_file": "Fichier journal", - "log_file_desc": "Chemin vers le fichier journal. Laissez vide pour désactiver la journalisation. Nécéssite un redémarrage.", + "log_file_desc": "Chemin d'accès au fichier de sortie de journalisation. Vide pour désactiver la journalisation du fichier. Nécessite un redémarrage.", "log_http": "Journaliser les accès HTTP", - "log_http_desc": "Journalise les accès HTTP dans le terminal. Nécéssite un redémarrage.", + "log_http_desc": "Journalise les accès HTTP dans le terminal. Nécessite un redémarrage.", "log_to_terminal": "Journaliser dans le terminal", - "log_to_terminal_desc": "Journalise dans le terminal en plus du fichier journal. Actif par défaut si la journalisation dans le fichier journal est désactivée. Nécéssite un redémarrage.", + "log_to_terminal_desc": "Journalise dans le terminal en complément d'un fichier. Toujours valide si la journalisation des fichiers est désactivée. Nécessite un redémarrage.", "maximum_session_age": "Durée maximum de session", - "maximum_session_age_desc": "Temps d'inactivité maximum avant la déconnexion automatique de la session.", + "maximum_session_age_desc": "Temps d'inactivité maximal avant expiration d'une session de connexion, en secondes.", "password": "Mot de passe", - "password_desc": "Mot de passe pour accéder à Stash. Laissez vide pour désactiver l'authentification.", - "stash-box_integration": "Integration de stash-box", + "password_desc": "Mot de passe pour accéder à Stash. Laisser vide pour désactiver l'authentification utilisateur", + "stash-box_integration": "Intégration de Stash-Box", "username": "Nom d'utilisateur", - "username_desc": "Nom d'utilisateur pour accéder à Stash. Laissez vide pour désactiver l'authentification." + "username_desc": "Nom d'utilisateur pour accéder à Stash. Laisser vide pour désactiver l'authentification utilisateur" }, - "cache_location": "Chemin vers le répertoire qui sera utilisé pour la mise en cache.", - "cache_path_head": "Répertorie de mise en cache", - "calculate_md5_and_ohash_desc": "Calculer l'empreinte MD5 en plus de OSHash. Activer cette option rendra plus lent le scan initial des fichiers. Le hachage des nom de fichiers doit être défini sur oshash pour désactiver le calcul du MD5.", + "cache_location": "Emplacement du répertoire du cache", + "cache_path_head": "Chemin du cache", + "calculate_md5_and_ohash_desc": "Calculer la somme de contrôle MD5 en complément de oshash. Son activation entraîne un ralentissement des analyses initiales. Le hachage du nom de fichier doit être défini sur oshash pour désactiver le calcul MD5.", "calculate_md5_and_ohash_label": "Calculer le MD5 pour les vidéos", "check_for_insecure_certificates": "Vérifier les certificats non sécurisés", - "check_for_insecure_certificates_desc": "Certains sites Web utilisent des certificats SSL non sécurisés. Lorsque cette option est décochée, le Scraper ignore la vérification des certificats non sécurisés et permet le Scraping de ces sites. Si vous obtenez une erreur de certificat lors du Scraping, décochez cette option.", - "chrome_cdp_path": "Chrome CDP", - "chrome_cdp_path_desc": "Chemin vers l'exécutable Chrome, ou une adresse distante (commençant par http:// ou https://, par exemple http://localhost:9222/json/version) vers une instance Chrome.", - "create_galleries_from_folders_desc": "Si coché, crée des Galeries à partir des dossiers contenant des images.", - "create_galleries_from_folders_label": "Créer des Galeries à partir des dossiers contenant des fichiers images", - "db_path_head": "Base de données", - "directory_locations_to_your_content": "Emplacements vers vos bibliothèques de contenu", - "excluded_image_gallery_patterns_desc": "Expressions régulières des fichiers ou chemins vers les images ou galeries à exclure de l'analyse et à ajouter au nettoyage.", - "excluded_image_gallery_patterns_head": "Images/galeries à exclure", - "excluded_video_patterns_desc": "Expressions régulières des fichiers ou chemins vers les vidéos à exclure de l'analyse et à ajouter au nettoyage.", - "excluded_video_patterns_head": "Vidéos à exclure", - "gallery_ext_desc": "Liste des extensions de fichier Archive qui seront considérés comme des Galeries d'Images. Séparez les extentions par une virgule.", - "gallery_ext_head": "Extensions de fichiers d'Archive pour les Galeries", - "generated_file_naming_hash_desc": "Utiliser MD5 ou OSHash pour le nommage des fichiers générés. Modifier ce réglage nécéssite que le MD5/OSHash ai déjà été généré pour toutes les Vidéos et Images. Après avoir modifié ce réglage, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", + "check_for_insecure_certificates_desc": "Certains sites utilisent des certificats SSL non sécurisés. Lorsque cette option est décochée, le scraper ignore la vérification des certificats non sécurisés et autorise le scraping de ces sites. Si vous obtenez une erreur de certificat lors du scraping, décochez cette option.", + "chrome_cdp_path": "Chemin Chrome CDP (Chrome Debugging Protocol)", + "chrome_cdp_path_desc": "Chemin de l'exécutable Chrome, ou adresse distante (commençant par http:// ou https://, par exemple http://localhost:9222/json/version) d'une instance de Chrome.", + "create_galleries_from_folders_desc": "Coché, crée des galeries à partir de dossiers contenant des images.", + "create_galleries_from_folders_label": "Créer des galeries à partir de dossiers contenant des images", + "db_path_head": "Chemin de la base de données", + "directory_locations_to_your_content": "Emplacements du répertoire de votre contenu", + "excluded_image_gallery_patterns_desc": "Expression régulière de fichiers images et galeries ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", + "excluded_image_gallery_patterns_head": "Modèles d'image ou galerie exclués", + "excluded_video_patterns_desc": "Expressions régulières de fichiers vidéo ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", + "excluded_video_patterns_head": "Modèles de vidéo exclués", + "gallery_ext_desc": "Liste séparée par des virgules des extensions de fichiers qui seront reconnues comme des archives zip de la galerie.", + "gallery_ext_head": "Extensions zip de la galerie", + "generated_file_naming_hash_desc": "Utilisez MD5 ou oshash pour le nommage des fichiers générés. Le modifier exige que toutes les scènes soient renseignées avec une valeur MD5/oshash appropriée. Après avoir modifié cette valeur, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", "generated_file_naming_hash_head": "Algorithme de hachage pour le nommage des fichiers générés", - "generated_files_location": "Emplacement pour les fichiers générés (marqueurs, apercus, sprites, etc)", - "generated_path_head": "Emplacement des fichiers générés", + "generated_files_location": "Emplacement du répertoire des fichiers générés (marqueurs de scène, aperçus de scène, sprites, etc.)", + "generated_path_head": "Chemin des fichiers générés", "hashing": "Hachage", - "image_ext_desc": "Liste des extensions de fichiers Images.", - "image_ext_head": "Extensions de fichiers Images", - "include_audio_desc": "Inclure l'audio dans la génération des aperçus.", + "image_ext_desc": "Liste délimitée par des virgules des extensions de fichiers qui seront reconnues comme des images.", + "image_ext_head": "Extensions des images", + "include_audio_desc": "Inclure le flux audio lors de la génération des aperçus.", "include_audio_head": "Inclure l'audio", "logging": "Journalisation", - "maximum_streaming_transcode_size_desc": "Résolution maximum pour la conversions des stream", - "maximum_streaming_transcode_size_head": "Résolution maximum de conversion pour les stream", - "maximum_transcode_size_desc": "Résolution maximum pour la conversions des fichiers", - "maximum_transcode_size_head": "Résolution maximum de conversion", + "maximum_streaming_transcode_size_desc": "Résolution maximale pour les flux transcodés", + "maximum_streaming_transcode_size_head": "Résolution maximale du flux transcodé", + "maximum_transcode_size_desc": "Résolution maximale pour les transcodes générés", + "maximum_transcode_size_head": "Résolution maximale de transcodage", "metadata_path": { - "description": "Emplacement du répertoire qui sera utilisé pour un export ou un import de métadonnées", - "heading": "Emplacement des fichiers de Métadonnées" + "description": "Emplacement du répertoire utilisé lors d'une exportation ou d'une importation complète", + "heading": "Chemin des métadonnées" + }, + "number_of_parallel_task_for_scan_generation_desc": "Définissez à 0 pour une détection automatique. Avertissement exécuter plus de tâches que ce qui est nécessaire pour atteindre une utilisation à 100% du processeur diminuera les performances et pourra causer d'autres problèmes.", + "number_of_parallel_task_for_scan_generation_head": "Nombre de tâches parallèles pour l'analyse et la génération", + "parallel_scan_head": "Analyse ou génération en parallèle", + "preview_generation": "Génération d'aperçu", + "python_path": { + "description": "Emplacement de l'exécutable python. Utilisé par les scrapers et les plugins. Si vide, python sera résolu à partir de l'environnement", + "heading": "Chemin de Python" }, - "number_of_parallel_task_for_scan_generation_desc": "0 pour détection automatique. Attention : une valeur trop élevée peut réduire les performances et causer d'autres problèmes.", - "number_of_parallel_task_for_scan_generation_head": "Nombre de tâches en parallèles pour le scan et la génération", - "parallel_scan_head": "Scan/Génération en parallèle", - "preview_generation": "Générer les aperçus", "scraper_user_agent": "User-Agent pour les Scraper", - "scraper_user_agent_desc": "Chaîne User-Agent utilisée dans les requêtes http lors du Scraping.", + "scraper_user_agent_desc": "Chaîne User-Agent utilisée dans les requêtes http lors du Scraping", "scrapers_path": { + "description": "Emplacement du répertoire des fichiers de configuration du scraper", "heading": "Chemin des scrapers" }, "scraping": "Scraping", - "sqlite_location": "Emplacement du fichier de base de données SQLite (nécéssite un redémarrage)", - "video_ext_desc": "Liste des extensions de fichiers Vidéos.", - "video_ext_head": "Extensions de fichiers Vidéo", + "sqlite_location": "Emplacement du fichier de base de données SQLite (nécessite un redémarrage)", + "video_ext_desc": "Liste délimitée par des virgules des extensions de fichiers qui seront reconnus comme des vidéos.", + "video_ext_head": "Extensions de fichiers vidéo", "video_head": "Vidéo" }, "library": { @@ -289,8 +313,8 @@ "scraping": { "entity_metadata": "{entityType} Métadonnées", "entity_scrapers": "{entityType} scrapers", - "excluded_tag_patterns_desc": "Expressions régulières pour l'exclusion de certains Tags des résultats de Scraping", - "excluded_tag_patterns_head": "Exclusion de Tags", + "excluded_tag_patterns_desc": "Expressions régulières de noms d'étiquettes à exclure des résultats de scraping", + "excluded_tag_patterns_head": "Modèles d'étiquette excluse", "scraper": "Scraper", "scrapers": "Scrapers", "search_by_name": "Recherche par nom", @@ -298,13 +322,13 @@ "supported_urls": "URLs" }, "stashbox": { - "add_instance": "Ajouter une instance stash-box", + "add_instance": "Ajouter une instance Stash-Box", "api_key": "Clé API", - "description": "Stash-box facilite le taggage automatique des vidéos et des acteur.trice.s en se basant sur l'empreinte et le nom des fichiers. Le Endpoint et la Clé API peuvent être trouvé dans votre compte stash-box. Si vous spécifiez plusieurs instances stash-box, le nom est requis.", - "endpoint": "Endpoint", - "graphql_endpoint": "Endpoint GraphQL", + "description": "Stash-Box simplifie l'étiquetage automatique des scènes et des performeurs en se basant sur les empreintes digitales et les noms de fichiers.\nLe point de connexion et la clé API se trouvent sur la page de votre compte de l'instance de stash-box. Les noms sont requis lorsque plusieurs instances sont ajoutées.", + "endpoint": "Point de connexion", + "graphql_endpoint": "Point de connexion GraphQL", "name": "Nom", - "title": "Endpoints Stash-box" + "title": "Points de connexion Stash-Box" }, "system": { "transcoding": "Transcodage" @@ -312,129 +336,144 @@ "tasks": { "added_job_to_queue": "{operation_name} ajouté(e) à la liste des tâches", "auto_tag": { - "auto_tagging_all_paths": "Marquage automatique de tous les chemins", - "auto_tagging_paths": "Marquage automatique des chemins suivants" + "auto_tagging_all_paths": "Étiquetage automatique de tous les chemins", + "auto_tagging_paths": "Étiquetage automatique des chemins suivants" }, - "auto_tag_based_on_filenames": "Taggage automatique basé sur le nom des fichiers.", - "auto_tagging": "Taggage automatique", - "backing_up_database": "Sauvegarder la base de données", - "backup_and_download": "Sauvegarde la base de données et télécharge le fichier de sauvegarde.", - "backup_database": "Sauvegarde la base de données au même emplacement que le fichier de base de données. Le fichier se sauvegarde aura le format suivant : {filename_format}", - "cleanup_desc": "Cherche et supprime les fichiers orphelins de la base de données. Cette action est destructive.", + "auto_tag_based_on_filenames": "Étiquetage automatique du contenu en se basant sur les noms de fichiers.", + "auto_tagging": "Étiquetage automatique", + "backing_up_database": "Sauvegarde de la base de données", + "backup_and_download": "Effectue une sauvegarde de la base de données et télécharge le fichier résultant.", + "backup_database": "Effectue une sauvegarde de la base de données dans le même répertoire que celle-ci, avec le format de nom de fichier {filename_format}", + "cleanup_desc": "Vérifier les fichiers manquants et les supprimer de la base de données. Cette action est destructive.", "data_management": "Gestion des données", - "defaults_set": "Les valeurs par défaut ont été définies et seront utilisées lorsque vous cliquerez sur le bouton {action} sur la page Tâches.", + "defaults_set": "Les valeurs par défaut ont été définies et seront utilisées en cliquant sur le bouton {action} de la page Tâches.", "dont_include_file_extension_as_part_of_the_title": "Ne pas inclure l'extension du fichier dans le titre", "empty_queue": "Aucune tâche n'est en cours d'exécution.", - "export_to_json": "Exporte la base de données au format JSON dans le dossier metadata.", + "export_to_json": "Exporte le contenu de la base de données au format JSON dans le répertoire des métadonnées.", "generate": { - "generating_from_paths": "Génération pour les scènes des chemins suivants", + "generating_from_paths": "Génération pour les scènes à partir des chemins suivants", "generating_scenes": "Génération pour {num} {scene}" }, - "generate_desc": "Génère les fichiers images, sprite, vidéo, vtt autres fichiers.", + "generate_desc": "Générer les images associées, images animées, vidéos, vtt et autres fichiers.", "generate_phashes_during_scan": "Générer des hachages perceptuels", "generate_phashes_during_scan_tooltip": "Pour la déduplication et l'identification de scènes.", - "generate_previews_during_scan": "Générer également les aperçus image pendant le scan (WebP animé, requis seulement si le Type d'apercu est définis sur Image Animée)", + "generate_previews_during_scan": "Générer des aperçus d'images animées", "generate_previews_during_scan_tooltip": "Générez des aperçus WebP animés (requis uniquement si le type d'aperçu est défini sur Image animée).", - "generate_sprites_during_scan": "Générer les Sprites pendant le scan (pour l'aperçu sur la barre de progression)", - "generate_thumbnails_during_scan": "Générer les miniatures pour les images", + "generate_sprites_during_scan": "Générer les images animées de progression", + "generate_thumbnails_during_scan": "Générer des vignettes pour les images", "generate_video_previews_during_scan": "Générer les aperçus", - "generate_video_previews_during_scan_tooltip": "Générez des aperçus vidéo qui s'activent lorsque vous survolez une scène", + "generate_video_previews_during_scan_tooltip": "Générer des aperçus vidéo joués lors du survol d'une scène", "generated_content": "Contenu généré", "identify": { - "and_create_missing": "et créer si manquant", - "create_missing": "Créer si manquant", + "and_create_missing": "et créer les manquants", + "create_missing": "Créer les manquants", "default_options": "Options par défaut", - "description": "Associe automatiquement les métadonnées aux Vidéos en utilisant stash-box et les Scrapers.", - "explicit_set_description": "Ces options seront utilisées par défaut à moins qu'une Source ne les définissent autrement.", + "description": "Définissez automatiquement les métadonnées de la scène en utilisant les sources Stash-Box et scraper.", + "explicit_set_description": "Les options suivantes seront utilisées si elles ne sont pas remplacées par les options spécifiques à la source.", "field": "Champ", "field_behaviour": "{strategy} {field}", - "field_options": "options pour les champs", + "field_options": "Options de champ", "heading": "Identifier", - "identifying_from_paths": "Identifier les vidéos à partir des emplacements suivants", + "identifying_from_paths": "Identifier des scènes à partir des chemins suivants", "identifying_scenes": "Identifier {num} {scene}", - "include_male_performers": "Inclure les acteurs hommes", - "set_cover_images": "Enregistrer l'image de couverture", - "set_organized": "Marquer comme Organisé", + "include_male_performers": "Inclure les performeurs masculins", + "set_cover_images": "Définir les images de couverture", + "set_organized": "Définir le drapeau organisé", "source": "Source", "source_options": "Options pour {source}", "sources": "Sources", "strategy": "Stratégie" }, - "import_from_exported_json": "Importez à partir du fichier JSON exporté dans le répertoire des métadonnées. Cela supprimera la base de données existante.", + "import_from_exported_json": "Importation à partir du JSON exporté dans le répertoire des métadonnées. Efface la base de données existante.", "incremental_import": "Importation incrémentielle à partir d'un fichier zip d'exportation fourni.", "job_queue": "File d'attente des tâches", "maintenance": "Maintenance", - "migrate_hash_files": "A utiliser si vous avez modifié l'algorithme de hachage pour le nommage des fichiers générés. Renomme les fichiers générés en fonction de l'algorithme de hachage sélectionné.", + "migrate_hash_files": "Utilisé après modification du hachage des fichiers générés pour renommer les existants au nouveau format.", "migrations": "Migrations", - "only_dry_run": "Effectuer un essai à blanc. Ne supprime rien du tout.", - "plugin_tasks": "Tâches des Plugins", + "only_dry_run": "Effectuer un essai à blanc. Ne supprime rien", + "plugin_tasks": "Tâches de Plugin", "scan": { - "scanning_all_paths": "Scanner tous les chemins", - "scanning_paths": "Scanner les chemins suivants" + "scanning_all_paths": "Analyse tous les chemins", + "scanning_paths": "Analyse les chemins suivants" }, - "scan_for_content_desc": "Scanner pour des nouveaux contenus et les ajouter à la base de données.", - "set_name_date_details_from_metadata_if_present": "Définissez le titre, la date et d'autres détails à partir des métadonnées du fichier (si présentes)" + "scan_for_content_desc": "Analyser le nouveau contenu et l'ajouter à la base de données.", + "set_name_date_details_from_metadata_if_present": "Définir le nom, date, détails à partir des métadonnées intégrées au fichier" }, "tools": { - "scene_duplicate_checker": "Vérificateur de doublons Vidéos", + "scene_duplicate_checker": "Vérificateur de doublons de scènes", "scene_filename_parser": { "add_field": "Ajouter un champ", "capitalize_title": "Titre en majuscule", "display_fields": "Afficher les champs", - "escape_chars": "Utilisez \\ pour échapper les caractères littéraux", - "filename": "Nom du fichier", - "filename_pattern": "Pattern des noms de fichier", - "ignore_organized": "Ignorer les Vidéos déjà Organisées", - "ignored_words": "Mots à ignorer", + "escape_chars": "Utiliser \\ pour échapper les caractères littéraux", + "filename": "Nom de fichier", + "filename_pattern": "Modèle de nom de fichier", + "ignore_organized": "Ignorer les scènes organisées", + "ignored_words": "Mots ignorés", "matches_with": "Correspond à {i}", - "select_parser_recipe": "Sélectionner une recette d'analyse", - "title": "Analyseur de nom de fichier Vidéos", - "whitespace_chars": "Espaces", + "select_parser_recipe": "Sélectionner une formule d'analyse", + "title": "Analyseur de noms de fichiers de scènes", + "whitespace_chars": "Caractères d'espacement", "whitespace_chars_desc": "Ces caractères seront remplacés par un espace dans le titre" }, - "scene_tools": "Outils Vidéos" + "scene_tools": "Outils de scène" }, "ui": { "basic_settings": "Paramètres de base", "custom_css": { "description": "La page doit être rafraichie pour que les changements prennent effet.", - "heading": "CSS personalisé", - "option_label": "Activer le CSS personalisé" + "heading": "CSS personnalisé", + "option_label": "Activer le CSS personnalisé" }, "delete_options": { - "description": "Réglages par défaut lors de la suppression des Vidéo, Images et Galeries.", + "description": "Réglages par défaut lors de la suppression d'images, galeries, et scènes.", "heading": "Options de suppression", "options": { - "delete_file": "Par défaut, effacer les fichiers", - "delete_generated_supporting_files": "Par défaut, effacer les fichiers générés" + "delete_file": "Supprimer le fichier par défaut", + "delete_generated_supporting_files": "Supprimer par défaut les fichiers associés générés" } }, "desktop_integration": { - "desktop_integration": "Intégration avec le Bureau", + "desktop_integration": "Intégration au bureau", + "notifications_enabled": "Activer les notifications", + "send_desktop_notifications_for_events": "Envoyer des notifications au bureau en cas d'événements", "skip_opening_browser": "Ne pas ouvrir de navigateur", - "skip_opening_browser_on_startup": "ne pas ouvrir de navigateur au démarrage de l'application" + "skip_opening_browser_on_startup": "Ignorer l'ouverture automatique du navigateur lors du démarrage" }, "editing": { "disable_dropdown_create": { - "description": "Désactive la possibilité de créer des nouveaux Actrices/Acteurs, Studio et Tags depuis la liste déroulante.", - "heading": "Désactiver la création depuis la liste déroulante." + "description": "Supprimer la possibilité de créer de nouveaux objets à partir des sélecteurs de liste déroulante", + "heading": "Désactiver la création depuis la liste déroulante" }, - "heading": "Edition" + "heading": "Édition" }, "funscript_offset": { - "description": "Décalage temporel (en millisecondes) à appliquer lors de la lecture des scripts interactifs.", + "description": "Décalage temporel en millisecondes pour la lecture des scripts interactifs.", "heading": "Décalage Funscript (ms)" }, + "handy_connection": { + "connect": "Connecter", + "server_offset": { + "heading": "Offset serveur" + }, + "status": { + "heading": "Statut de connexion Handy" + }, + "sync": "Synchroniser" + }, "handy_connection_key": { - "description": "Clé de connexion Handy pour les Vidéos interactives. Définir cette clé permettra à Stash de partager les informations de votre scène actuelle avec handyfeeling.com", + "description": "Clé de connexion Handy à utiliser pour les scènes interactives. En définissant cette clé, vous permettez à Stash de partager les informations de votre scène actuelle avec handyfeeling.com", "heading": "Clé de connexion Handy" }, + "image_lightbox": { + "heading": "Visionneuse d'images" + }, "images": { "heading": "Images", "options": { "write_image_thumbnails": { - "description": "Écrire les miniatures des images sur le disque lorsqu'elles sont générées à la volée", - "heading": "Enregistrer les miniatures des images" + "description": "Écrire les vignettes des images sur le disque lorsqu'elles sont générées à la volée", + "heading": "Enregistrer les vignettes des images" } } }, @@ -443,8 +482,8 @@ "heading": "Langue" }, "max_loop_duration": { - "description": "Durée maximum de la vidéo pendant laquelle le lecteur rebouclera la vidéo - 0 pour désactiver", - "heading": "Durée maximum de rebouclage" + "description": "Durée maximale de la scène pendant laquelle le lecteur bouclera la vidéo - 0 pour désactiver", + "heading": "Durée maximale de la boucle" }, "menu_items": { "description": "Afficher ou masquer différents types de contenus dans la barre de navigation", @@ -453,13 +492,13 @@ "performers": { "options": { "image_location": { - "description": "Chemin vers l'image à utiliser pour les Actrices/Acteurs qui n'ont pas d'image. Laissez vide pour utiliser l'image par défaut de Stash.", - "heading": "Image par défaut personalisée pour les actrices/acteurs" + "description": "Chemin personnalisé pour les images par défaut de performeur. Laisser vide pour utiliser les valeurs par défaut intégrées", + "heading": "Chemin de l'image du performeur personnalisé" } } }, "preview_type": { - "description": "Configuration des éléments du Mur", + "description": "Configuration des éléments du mur", "heading": "Type d'aperçu", "options": { "animated": "Image animée", @@ -468,34 +507,40 @@ } }, "scene_list": { - "heading": "Liste Vidéo", + "heading": "Liste de scène", "options": { - "show_studio_as_text": "Afficher les Studios en tant que texte" + "show_studio_as_text": "Afficher les studios sous format texte" } }, "scene_player": { - "heading": "Lecteur Vidéo", + "heading": "Lecteur de scène", "options": { - "auto_start_video": "Lecture automatique", + "auto_start_video": "Démarrer automatiquement la vidéo", "auto_start_video_on_play_selected": { - "description": "Lancer automatiquement la lecture de la vidéo de la scène lorsque « lecture » est sélectionné ou qu'une scène aléatoire est sélectionnée à partir de la page de la scène", - "heading": "Démarrer automatiquement la vidéo lorsque \"play\" est sélectionné" + "description": "Démarrer automatiquement les scènes vidéo lorsque lecture est sélectionnée ou aléatoire à partir de la page Scènes", + "heading": "Démarrer automatiquement la vidéo lorsque lecture est sélectionnée" }, "continue_playlist_default": { - "description": "Lire la scène suivante dans la file d'attente lorsque la vidéo est terminée", + "description": "Lire la scène suivante dans la file d'attente lorsque la vidéo se termine", "heading": "Continuer la liste de lecture par défaut" - } + }, + "show_scrubber": "Montrer la barre de progression" } }, "scene_wall": { + "heading": "Mur de scènes et marqueurs", "options": { - "display_title": "Afficher le titre et les tags", + "display_title": "Afficher le titre et étiquettes", "toggle_sound": "Activer le son" } }, + "scroll_attempts_before_change": { + "description": "Nombre de tentatives de défilement avant de passer à l'élément suivant/précédent. S'applique uniquement au mode de défilement Pan Y.", + "heading": "Tentatives de défilement avant transition" + }, "slideshow_delay": { - "description": "Diaporama disponible dans Galeries en mode de visionnage Mur", - "heading": "Durée de défilement du diaporama" + "description": "Le diaporama est disponible dans galerie en mode de vue mural", + "heading": "Délai du diaporama (secondes)" }, "title": "Interface utilisateur" } @@ -507,14 +552,14 @@ "images": "{count, plural, one {Image} other {Images}}", "markers": "{count, plural, one {Marqueur} other {Marqueurs}}", "movies": "{count, plural, one {Film} other {Films}}", - "performers": "{count, plural, one {Actrice/Acteur} other {Actrices/Acteurs}}", - "scenes": "{count, plural, one {Vidéo} other {Vidéos}}", + "performers": "{count, plural, one {Performeur} other {Performeurs}}", + "scenes": "{count, plural, one {Scène} other {Scènes}}", "studios": "{count, plural, one {Studio} other {Studios}}", - "tags": "{count, plural, one {Tag} other {Tags}}" + "tags": "{count, plural, one {Étiquette} other {Étiquettes}}" }, "country": "Pays", "cover_image": "Image de couverture", - "created_at": "Date de création", + "created_at": "Créé le", "criterion": { "greater_than": "Supérieur à", "less_than": "Inférieur à", @@ -528,52 +573,52 @@ "greater_than": "est plus grand que", "includes": "contient", "includes_all": "contient tout", - "is_null": "est null", + "is_null": "est nul", "less_than": "est plus petit que", - "matches_regex": "match l'expression régulière", + "matches_regex": "correspond à l'expression régulière", "not_between": "en dehors", "not_equals": "n'est pas égal à", - "not_matches_regex": "ne match pas l'expression régulière", - "not_null": "n'est pas null" + "not_matches_regex": "ne correspond pas à l'expression régulière", + "not_null": "n'est pas nul" }, - "custom": "Personalisé", + "custom": "Personnalisé", "date": "Date", - "death_date": "Date de décès", - "death_year": "Année de décès", + "death_date": "Date du décès", + "death_year": "Année du décès", "descending": "Descendant", "detail": "Détail", "details": "Détails", - "developmentVersion": "Version de Développement", + "developmentVersion": "Version de développement", "dialogs": { "aliases_must_be_unique": "Les alias doivent être uniques", "delete_alert": "{count, plural, one {Ce/Cette {singularEntity} sera supprimé(e)} other {Ces {pluralEntity} seront supprimé(e)s}} définitivement :", "delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?", - "delete_entity_desc": "{count, plural, one {Êtes-vous sûr de vouloir supprimer cette {singularEntity} ? À moins que le fichier ne soit également supprimé, cette {singularEntity} sera ajoutée à nouveau lors du prochain Scan.} other {Êtes-vous sûr de vouloir supprimer ces {pluralEntity} ? À moins que les fichiers ne soient également supprimés, ces {pluralEntity} seront ajoutées à nouveau lors du prochain Scan.}}", + "delete_entity_desc": "{count, plural, one {Êtes-vous sûr de vouloir supprimer cette {singularEntity} ? À moins que le fichier ne soit également supprimé, cette {singularEntity} sera ajoutée à nouveau lors de la prochaine analyse.} other {Êtes-vous sûr de vouloir supprimer ces {pluralEntity} ? À moins que les fichiers ne soient également supprimés, ces {pluralEntity} seront ajoutées à nouveau lors de la prochaine analyse.}}", "delete_entity_title": "{count, plural, one {Delete {singularEntity}} other {Delete {pluralEntity}}}", - "delete_galleries_extra": "…ainsi que tout fichier image non-associé à une autre Galerie.", - "delete_gallery_files": "Supprime le dossier ou l'archive de la galerie, ainsi que toute image non associées à une autre galerie.", - "delete_object_desc": "Êtes-vous sûr de vouloir supprimer : {count, plural, one {this {singularEntity}} other {these {pluralEntity}}} ?", + "delete_galleries_extra": "…ainsi que tous fichiers image qui ne sont pas associés à une autre galerie.", + "delete_gallery_files": "Supprime le répertoire ou l'archive zip de la galerie et toutes images qui ne sont pas associées à une autre galerie.", + "delete_object_desc": "Êtes-vous sûr de vouloir supprimer {count, plural, one {this {singularEntity}} other {these {pluralEntity}}} ?", "delete_object_overflow": "…et {count} {count, plural, one {autre {singularEntity}} other {autres {pluralEntity}}}.", "delete_object_title": "Supprimer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", - "edit_entity_title": "Editer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", - "export_include_related_objects": "Inclure les entités liées", + "edit_entity_title": "Éditer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "export_include_related_objects": "Inclure les objets liés dans l'exportation", "export_title": "Export", "lightbox": { - "delay": "Délais (Secondes)", + "delay": "Délai (Secondes)", "display_mode": { - "fit_horizontally": "Adapter horizontalement", + "fit_horizontally": "Ajustement horizontal", "fit_to_screen": "Adapter à l'écran", "label": "Mode d'affichage", "original": "Original" }, "options": "Options", - "reset_zoom_on_nav": "Remise à zero du niveau de zoom au changement d'image", + "reset_zoom_on_nav": "Réinitialisation du facteur de zoom lors d'un changement d'image", "scale_up": { - "description": "Agrandir l'image pour adapter à l'écran", - "label": "Adapter à l'écran" + "description": "Redimensionner les petites images pour les adapter à l'écran", + "label": "Mise à l'échelle" }, "scroll_mode": { - "description": "Maintenir MAJ pour utiliser temporairement l'autre mode.", + "description": "Maintenir la touche shift pour utiliser temporairement l'autre mode.", "label": "Mode de défilement", "pan_y": "Panoramique axe Y", "zoom": "Zoom" @@ -583,59 +628,62 @@ "destination": "Destination", "source": "Source" }, - "overwrite_filter_confirm": "Êtes-vous sûr de vouloir écraser le filtre sauvegardé {entityName} ?", + "overwrite_filter_confirm": "Êtes-vous sûr de vouloir remplacer la requête sauvegardée existante {entityName} ?", "scene_gen": { - "force_transcodes": "Forcer la génération de transcodage", - "force_transcodes_tooltip": "Par défaut, les transcodages ne sont générés que lorsque le fichier vidéo n'est pas pris en charge dans le navigateur. Lorsqu'il est activé, les transcodes seront générés même lorsque le fichier vidéo semble être pris en charge dans le navigateur.", - "image_previews": "Aperçus image (Image WebP animée. Requis seulement si le type d'Aperçu est défini sur Image Animée)", - "image_previews_tooltip": "Aperçus animés (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", - "interactive_heatmap_speed": "Générer des cartes thermiques et des vitesses pour des scènes interactives", - "marker_image_previews": "Aperçus Marqueurs (Image WebP animée. Requis seulement si le type d'aperçu est défini sur Image Animée)", - "marker_image_previews_tooltip": "Aperçus animé des marqueurs (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", - "marker_screenshots": "Capture d'écran Marqueurs (Image JPG fixe. Requis seulement si le type d'Aperçu est défini sur Image Fixe)", + "force_transcodes": "Forcer la génération du transcodage", + "force_transcodes_tooltip": "Par défaut, les transcodes ne sont générés que lorsque le fichier vidéo n'est pas pris en charge par le navigateur. Activé, les transcodes seront générés même si le fichier vidéo semble être pris en charge par le navigateur.", + "image_previews": "Aperçus d'images animées", + "image_previews_tooltip": "Aperçu WebP animé, requis uniquement si le mode d'aperçu est défini sur Image animée.", + "interactive_heatmap_speed": "Générer des cartes thermiques et des vitesses pour les scènes interactives", + "marker_image_previews": "Aperçu des images animées par marqueur", + "marker_image_previews_tooltip": "Aperçus WebP de marqueurs animés, requis uniquement si le mode d'aperçu est défini sur Image animée.", + "marker_screenshots": "Captures d'écran du marqueur", + "marker_screenshots_tooltip": "Marquer les images JPG statiques, requis uniquement si le mode d'aperçu est défini sur Image statique.", "markers": "Aperçus des Marqueurs", - "markers_tooltip": "Vidéo de 20 secondes qui commence au timecode indiqué.", - "overwrite": "Ecraser les fichiers générés existants", - "phash": "Hachage perceptif (pour la déduplication)", - "preview_exclude_end_time_desc": "Exclure les x dernières secondes de la vidéo pour la génération de l'aperçu. La valeur peut-être exprimée en secondes ou bien en pourcentage (ex : 2%) de la durée totale de la vidéo.", - "preview_exclude_end_time_head": "Exclure à la fin", - "preview_exclude_start_time_desc": "Exclure les x premières secondes de la vidéo pour la génération de l'aperçu. La valeur peut-être exprimée en secondes ou bien en pourcentage (ex : 2%) de la durée totale de la vidéo.", - "preview_exclude_start_time_head": "Exclure au début", - "preview_generation_options": "Options des générations d'aperçu", - "preview_options": "Options d'Aperçus", - "preview_preset_desc": "Le Preset d'encodage régule la taille, la qualité et le temps d'encodage des aperçus. Les Preset plus bas que “slow” n'apportent pas de gain significatif et ne sont pas recommandés.", - "preview_preset_head": "Preset d'encodage de l'aperçu", - "preview_seg_count_desc": "Nombre total de segments dans un aperçu.", - "preview_seg_count_head": "Nombre de segments dans un aperçu", - "preview_seg_duration_desc": "Durée de chaque segment d'aperçu (en secondes).", - "preview_seg_duration_head": "Durée d'un segment d'aperçu", - "sprites": "Sprites (pour l'aperçu sur la barre de progression)", - "sprites_tooltip": "Sprites (pour l'aperçu sur la barre de progression)", - "transcodes": "Transcodages", - "transcodes_tooltip": "Conversion au format MP4 des fichiers vidéo dont le format n'est pas supporté par le lecteur vidéo", + "markers_tooltip": "Vidéos de 20 secondes qui débutent au repère temporel donné.", + "override_preview_generation_options": "Remplacer les options de génération d'aperçu", + "override_preview_generation_options_desc": "Remplacer les options de génération d'aperçu pour cette opération. Les valeurs par défaut sont définies dans Système -> Génération d'aperçus.", + "overwrite": "Remplacer les fichiers générés existants", + "phash": "Hachages perceptuels (pour la déduplication)", + "preview_exclude_end_time_desc": "Exclure les x dernières secondes des aperçus de la scène. Cela peut être une valeur en secondes, ou un pourcentage (par exemple 2%) de la durée totale de la scène.", + "preview_exclude_end_time_head": "Exclure le temps de fin", + "preview_exclude_start_time_desc": "Exclure les x premières secondes des aperçus de la scène. Cela peut être une valeur en secondes, ou un pourcentage (par exemple 2%) de la durée totale de la scène.", + "preview_exclude_start_time_head": "Exclure le temps de départ", + "preview_generation_options": "Options de génération d'aperçus", + "preview_options": "Options d'aperçu", + "preview_preset_desc": "Le préréglage règle la taille, la qualité et le temps d'encodage de la génération d'aperçu. Les préréglages autres que \"slow\" ont un résultat moindre et ne sont pas recommandés.", + "preview_preset_head": "Préréglage de l'encodage d'aperçu", + "preview_seg_count_desc": "Nombre de segments dans les fichiers de l'aperçu.", + "preview_seg_count_head": "Nombre de segments dans l'aperçu", + "preview_seg_duration_desc": "Durée de chaque segment d'aperçu, en secondes.", + "preview_seg_duration_head": "Durée du segment d'aperçu", + "sprites": "Sprites de progression de scène", + "sprites_tooltip": "Sprites (pour la progression de scène)", + "transcodes": "Transcoder", + "transcodes_tooltip": "Conversion en MP4 des formats vidéo non pris en charge", "video_previews": "Aperçus", - "video_previews_tooltip": "Lire les aperçus vidéos lors du survol des vidéos" + "video_previews_tooltip": "Prévisualisation de la vidéo lors du survol d'une scène" }, - "scenes_found": "{count} Vidéo(s) trouvée(s)", + "scenes_found": "{count} scènes trouvées", "scrape_entity_query": "Requête de Scrape {entity_type}", - "scrape_entity_title": "Résultat du Scraping {entity_type}", + "scrape_entity_title": "Résultats du Scraping {entity_type}", "scrape_results_existing": "Existant", "scrape_results_scraped": "Scraped", "set_image_url_title": "URL de l'image", - "unsaved_changes": "Changements non-sauvegardés. Êtes-vous sûr de vouloir quitter ?" + "unsaved_changes": "Modifications non sauvegardées. Vous êtes sûr de vouloir quitter ?" }, - "dimensions": "Dimensions de l'image", + "dimensions": "Dimensions", "director": "Réalisateur", "display_mode": { "grid": "Grille", "list": "Liste", - "tagger": "Taggueur", + "tagger": "Étiqueteur", "unknown": "Inconnu", "wall": "Mur" }, "donate": "Faire un don", "dupe_check": { - "description": "Les précisions plus faibles que 'Exacte' peuvent prendre plus de temps à être calculées. Les niveaux de précisions faibles peuvent également mener à des résultats faux-positifs.", + "description": "Les niveaux en-deça de \"Exact\" peuvent prendre plus de temps à calculer. Des faux positifs peuvent également être retournés à de faibles précisions.", "found_sets": "{setCount, plural, one{# ensemble de doublons trouvé.} other {# ensembles de doublons trouvés.}}", "options": { "exact": "Exacte", @@ -643,9 +691,10 @@ "low": "Basse", "medium": "Moyenne" }, - "search_accuracy_label": "Précision", - "title": "Scènes en double" + "search_accuracy_label": "La pertinence de la recherche", + "title": "Scènes dupliquées" }, + "duplicated_phash": "Dupliqué (phash)", "duration": "Durée", "effect_filters": { "aspect": "Aspect", @@ -658,68 +707,86 @@ "hue": "Teinte", "name": "Filtres", "name_transforms": "Transformations", - "red": "Red", - "reset_filters": "Annuler les filtres", - "reset_transforms": "Annuler les transformations", + "red": "Rouge", + "reset_filters": "Rétablir les filtres", + "reset_transforms": "Rétablir les transformations", "rotate": "Rotation", - "rotate_left_and_scale": "Rotation Gauche & Dimensionnement", - "rotate_right_and_scale": "Rotation Droite & Dimensionnement", + "rotate_left_and_scale": "Rotation à gauche et mise à l'échelle", + "rotate_right_and_scale": "Rotation à droite et mise à l'échelle", "saturation": "Saturation", - "scale": "Dimension", + "scale": "Mise à l'échelle", "warmth": "Chaleur" }, + "empty_server": "Ajoutez quelques scènes à votre serveur pour afficher les recommandations sur cette page.", "ethnicity": "Ethnicité", + "existing_value": "valeur existante", "eye_color": "Couleur des yeux", - "fake_tits": "Implants mammaires", + "fake_tits": "Faux seins", "false": "Faux", "favourite": "Favoris", "file": "fichier", "file_info": "Infos fichier", - "file_mod_time": "Date de modification fichier", - "files": "fichier", + "file_mod_time": "Date de modification du fichier", + "files": "fichiers", "filesize": "Taille du fichier", "filter": "Filtre", "filter_name": "Nom du filtre", "filters": "Filtres", - "framerate": "Fréquence d'images", - "frames_per_second": "{value} ips", + "framerate": "Fréquence de rafraîchissement", + "frames_per_second": "{value} images par seconde", + "front_page": { + "types": { + "premade_filter": "Filtre primaire", + "saved_filter": "Filtre sauvegardé" + } + }, "galleries": "Galeries", "gallery": "Galerie", - "gallery_count": "Nombre de Galeries", + "gallery_count": "Nombre de galeries", "gender": "Genre", "gender_types": { "FEMALE": "Femme", "INTERSEX": "Intersexe", "MALE": "Homme", - "NON_BINARY": "Non Binaire", + "NON_BINARY": "Non binaire", "TRANSGENDER_FEMALE": "Femme transgenre", "TRANSGENDER_MALE": "Homme transgenre" }, "hair_color": "Couleur des cheveux", - "hasMarkers": "Possède des marqueurs", - "height": "Taille", + "handy_connection_status": { + "connecting": "Connexion", + "disconnected": "Déconnecté", + "error": "Erreur de connexion à Handy", + "missing": "Manquant", + "ready": "Prêt", + "syncing": "Synchronisation avec le serveur", + "uploading": "Script de chargement" + }, + "hasMarkers": "Dispose de marqueurs", + "height": "Hauteur", "help": "Aide", + "ignore_auto_tag": "Ignorer l'étiquetage automatique", "image": "Image", "image_count": "Nombre d'Images", "images": "Images", - "include_parent_tags": "Inclure les Tags parents", - "include_sub_studios": "Inclure les studios affiliés / filiales", - "include_sub_tags": "Inclure les sous-Tags", + "include_parent_tags": "Inclure les étiquettes parentes", + "include_sub_studios": "Inclure les studios affiliés", + "include_sub_tags": "Inclure les sous-étiquettes", "instagram": "Instagram", "interactive": "Interactif", "interactive_speed": "Vitesse interactive", - "isMissing": "Manquant", + "isMissing": "Est manquant", "library": "Bibliothèque", "loading": { "generic": "Chargement…" }, - "marker_count": "Nombre de Marqueurs", + "marker_count": "Nombre de marqueurs", "markers": "Marqueurs", "measurements": "Mensurations", "media_info": { - "audio_codec": "Codec Audio", + "audio_codec": "Codec audio", "checksum": "Somme de contrôle", - "downloaded_from": "Téléchargé de", + "downloaded_from": "Téléchargé depuis", "hash": "Hachage", "interactive_speed": "Vitesse interactive", "performer_card": { @@ -730,7 +797,7 @@ "stream": "Stream", "video_codec": "Codec vidéo" }, - "megabits_per_second": "{value} Mbps", + "megabits_per_second": "{value} mégabits par seconde", "metadata": "Métadonnées", "movie": "Film", "movie_scene_number": "Nombre de Scènes de films", @@ -738,7 +805,7 @@ "name": "Nom", "new": "Nouveau", "none": "Aucun", - "o_counter": "O-mètre", + "o_counter": "O-Compteur", "operations": "Opérations", "organized": "Organisé", "pagination": { @@ -749,30 +816,70 @@ }, "parent_of": "Parent de {children}", "parent_studios": "Studio parent", - "parent_tag_count": "Nombre de Tags Parents", - "parent_tags": "Tag parent", + "parent_tag_count": "Nombre d'étiquettes parentes", + "parent_tags": "Étiquettes parentes", "part_of": "Fait partie de {parent}", "path": "Chemin", - "performer": "Actrice/Acteur", - "performerTags": "Tags d'Actrice/Acteur", - "performer_count": "Nombre d'Actrices/Acteurs", - "performer_image": "Photo", - "performers": "Actrices/Acteurs", + "perceptual_similarity": "Similitude perceptuelle (phash)", + "performer": "Performeur", + "performerTags": "Étiquettes de performeur", + "performer_age": "Âge du performeur", + "performer_count": "Nombre de performeur", + "performer_favorite": "Performeur favori", + "performer_image": "Photo du performeur", + "performer_tagger": { + "add_new_performers": "Ajouter de nouveaux performeurs", + "any_names_entered_will_be_queried": "Tout nom saisi sera demandé à l'instance distante StashBox et ajouté si trouvé. Seules les correspondances exactes seront considérées comme équivalentes.", + "batch_add_performers": "Ajouter des performeurs par lots", + "batch_update_performers": "Mises à jour des performeurs par lots", + "config": { + "active_stash-box_instance": "Instance stash-box active :", + "edit_excluded_fields": "Modifier les champs exclus", + "excluded_fields": "Champs exclus :", + "no_fields_are_excluded": "Aucun champ n'est exclu", + "no_instances_found": "Aucune instance trouvée", + "these_fields_will_not_be_changed_when_updating_performers": "Ces champs ne seront pas modifiés lors de la mise à jour des performeurs." + }, + "current_page": "Page actuelle", + "failed_to_save_performer": "Échec pour sauvegarder le performeur \"{performer}\"", + "name_already_exists": "Le nom existe déjà", + "network_error": "Erreur réseau", + "no_results_found": "Aucun résultat trouvé.", + "number_of_performers_will_be_processed": "{performer_count} performeurs seront traités", + "performer_already_tagged": "Performeur déjà étiqueté", + "performer_names_separated_by_comma": "Noms des performeurs séparés par une virgule", + "performer_selection": "Sélection du performeur", + "performer_successfully_tagged": "Performeur étiqueté avec succès :", + "query_all_performers_in_the_database": "Tous les performeurs dans la base de données", + "refresh_tagged_performers": "Actualiser les performeurs étiquetés", + "refreshing_will_update_the_data": "Une actualisation mettra à jour les données de tous les performeurs étiquetés de l'instance stash-box.", + "status_tagging_job_queued": "Statut : Tâche d'étiquetage en file d'attente", + "status_tagging_performers": "Statut : Étiquetage des performeurs", + "tag_status": "Statut de l'étiquette", + "to_use_the_performer_tagger": "Pour utiliser l'étiqueteur de performeurs, une instance stash-box doit être configurée.", + "untagged_performers": "Performeurs non étiquetés", + "update_performer": "Mise à jour du performeur", + "update_performers": "Mise à jour des performeurs", + "updating_untagged_performers_description": "Une mise à jour des performeurs non étiquetés essaiera de faire correspondre tous les performeurs qui n'ont pas de StashID et actualisera les métadonnées." + }, + "performers": "Performeurs", "piercings": "Piercings", "queue": "File de lecture", "random": "Aléatoire", "rating": "Note", + "recently_added_objects": "Récemment ajouté {objects}", + "recently_released_objects": "Récemment sortis {objects}", "resolution": "Résolution", - "scene": "Vidéo", - "sceneTagger": "Taggueur de Scène", - "sceneTags": "Tags de Scène", - "scene_count": "Nombre de Vidéos", - "scene_id": "Vidéo ID", - "scenes": "Vidéos", - "scenes_updated_at": "Scène modifiée le", + "scene": "Scène", + "sceneTagger": "Étiqueteur de scènes", + "sceneTags": "Étiquettes de scène", + "scene_count": "Nombre de scènes", + "scene_id": "ID de scène", + "scenes": "Scènes", + "scenes_updated_at": "Scène actualisée le", "search_filter": { "add_filter": "Ajouter un filtre", - "name": "Filtres", + "name": "Filtre", "saved_filters": "Filtres sauvegardés", "update_filter": "Mise à jour du filtre" }, @@ -780,10 +887,10 @@ "settings": "Paramètres", "setup": { "confirm": { - "almost_ready": "Nous sommes presque prêts à terminer la configuration. Veuillez confirmer les paramètres suivants. Vous pouvez revenir en arrière pour modifier tout ce qui est incorrect. Si tout semble bon, cliquez sur Confirmer pour créer votre système.", + "almost_ready": "Nous sommes presque prêts à terminer la configuration. Confirmez les paramètres suivants. Vous pouvez revenir en arrière pour modifier toute erreur. Si tout semble correct, cliquez sur Confirmer pour créer votre système.", "configuration_file_location": "Emplacement du fichier de configuration :", - "database_file_path": "Chemin du fichier de la base de données", - "default_db_location": "/stash-go.sqlite", + "database_file_path": "Chemin du fichier de base de données", + "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Répertoire généré", "nearly_there": "Nous y sommes presque !", @@ -791,114 +898,125 @@ }, "creating": { "creating_your_system": "Création de votre système", - "ffmpeg_notice": "Si ffmpeg n'est pas disponible, soyez patient le temps que Stash le télécharge. Regardez la sortie de la console pour voir la progression du téléchargement." + "ffmpeg_notice": "Si ffmpeg n'est pas encore dans vos chemins, veuillez être patient pendant que stash le télécharge. Consultez la sortie de la console pour voir la progression du téléchargement." }, "errors": { - "something_went_wrong": "Oh non ! Quelque chose n'a pas fonctionné !", - "something_went_wrong_description": "Si vous pensez qu'il peut y avoir une erreur avec les données fournies, veuillez cliquer en arrière pour la corriger. Sinon, veuillez ouvrir un ticket sur {githubLink} ou demander de l'aide à {discordLink}.", - "something_went_wrong_while_setting_up_your_system": "Une erreur s'est produite lors de la configuration de votre système. Voici l'erreur que nous avons reçue : {error}" + "something_went_wrong": "Oh non ! Quelque chose a mal tourné !", + "something_went_wrong_description": "Si cela ressemble à un problème avec vos saisies, continuez et cliquez sur retour pour les corriger. Sinon, créez un bogue sur {githubLink} ou demandez de l'aide sur {discordLink}.", + "something_went_wrong_while_setting_up_your_system": "Un problème est survenu lors de la configuration de votre système. Voici l'erreur que nous avons reçue : {error}" + }, + "folder": { + "file_path": "Chemin de fichier", + "up_dir": "Remonter d'un répertoire" }, "github_repository": "Dépôt Github", "migrate": { - "backup_database_path_leave_empty_to_disable_backup": "Chemin de la sauvegarde de la base de données (laissez vide pour désactiver la sauvegarde) :", + "backup_database_path_leave_empty_to_disable_backup": "Chemin de la base de données de sauvegarde (laissez vide pour désactiver la sauvegarde) :", "backup_recommended": "Il est recommandé de sauvegarder votre base de données existante avant de procéder à la migration. Nous pouvons le faire pour vous, en faisant une copie de votre base de données dans {defaultBackupPath}.", "migrating_database": "Migration de la base de données", - "migration_failed": "Échec de la migration", - "migration_failed_error": "L'erreur suivante s'est produite lors de la migration de la base de données :", - "migration_failed_help": "Veuillez apporter les corrections nécessaires et réessayer. Sinon, signalez un bogue sur le {githubLink} ou cherchez de l'aide dans le {discordLink}.", - "migration_irreversible_warning": "Le processus de migration n'est pas réversible. Une fois la migration effectuée, votre base de données sera incompatible avec les versions précédentes de Stash.", + "migration_failed": "La migration a échoué", + "migration_failed_error": "L'erreur suivante a été rencontrée lors de la migration de la base de données :", + "migration_failed_help": "Veuillez apporter les corrections nécessaires et réessayer. Sinon, signalez un bogue sur {githubLink} ou demandez de l'aide sur {discordLink}.", + "migration_irreversible_warning": "Le processus de migration des schémas n'est pas réversible. Une fois la migration effectuée, votre base de données sera incompatible avec les versions précédentes de Stash.", "migration_required": "Migration requise", - "perform_schema_migration": "Effectuer la migration", - "schema_too_old": "La version de la base de données est {databaseSchema} et doit être mise à jour vers {appSchema} . Cette version de Stash ne fonctionnera pas sans migration de la base de données." + "perform_schema_migration": "Procéder à la migration des schémas", + "schema_too_old": "La version du schéma de votre base de données Stash actuelle est {databaseSchema} et doit être migrée vers la version {appSchema}. Cette version de Stash ne fonctionnera pas sans migration de la base de données." }, "paths": { "database_filename_empty_for_default": "Nom de fichier de la base de données (vide par défaut)", - "description": "Ensuite, nous devons déterminer où trouver votre collection de porno, où stocker la base de données de stockage et les fichiers générés. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", + "description": "Ensuite, nous devons déterminer où trouver votre collection pornographique, où stocker la base de données Stash et les fichiers générés. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", "path_to_generated_directory_empty_for_default": "Chemin vers le répertoire généré (vide par défaut)", "set_up_your_paths": "Configurez vos chemins", - "stash_alert": "Aucun chemin de bibliothèque n'a été sélectionné. Aucun média ne pourra être analysé dans Stash. Es-tu sûr ?", + "stash_alert": "Aucun chemin de bibliothèque n'a été sélectionné. Aucun média ne pourra être analysé dans Stash. En êtes-vous sûr ?", "where_can_stash_store_its_database": "Où Stash peut-il stocker sa base de données ?", - "where_can_stash_store_its_database_description": "Stash utilise une base de données SQLite pour stocker les métadonnées de votre collection. Par défaut, il sera créé en tant que stash-go.sqlite dans le répertoire où se trouve votre fichier de configuration. Si vous souhaitez modifier cela, veuillez entrer un nom de fichier avec un chemin absolu ou relatif (vers le répertoire de travail actuel).", - "where_can_stash_store_its_generated_content": "Où Stash peut-il stocker le contenu généré ?", - "where_can_stash_store_its_generated_content_description": "Afin de fournir des miniatures, des aperçus et des sprites, Stash génère des images et des vidéos. Cela inclut également les transcodages pour les formats de fichiers non pris en charge. Par défaut, Stash créera un répertoire generated dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier l'emplacement de stockage de ce média généré, veuillez saisir un chemin absolu ou relatif (au répertoire de travail actuel). Stash créera ce répertoire s'il n'existe pas déjà.", + "where_can_stash_store_its_database_description": "Stash utilise une base de données sqlite pour stocker vos métadonnées pornographiques. Par défaut, cette base sera créée en tant que stash-go.sqlite dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier cela, saisissez un nom de fichier absolu ou relatif ( vers le répertoire de travail actuel).", + "where_can_stash_store_its_generated_content": "Où Stash peut-il stocker son contenu généré ?", + "where_can_stash_store_its_generated_content_description": "Afin de produire les vignettes, les aperçus et les images animées, Stash génère des images et des vidéos. Cela inclut également les transcodes pour les formats de fichiers non pris en charge. Par défaut, Stash crée un répertoire generated dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier l'emplacement où seront stockés les médias générés, veuillez saisir un chemin absolu ou relatif ( vers le répertoire de travail actuel). Stash créera ce répertoire s'il n'existe pas déjà.", "where_is_your_porn_located": "Où se trouve votre porno ?", - "where_is_your_porn_located_description": "Ajoutez des répertoires contenant vos vidéos et images porno. Stash utilisera ces répertoires pour rechercher des vidéos et des images lors de l'analyse." + "where_is_your_porn_located_description": "Ajoutez des répertoires contenant vos vidéos et images pornographiques. Stash utilisera ces répertoires pour rechercher les vidéos et les images lors de l'analyse." }, "stash_setup_wizard": "Assistant de configuration de Stash", "success": { "getting_help": "Obtenir de l'aide", "help_links": "Si vous rencontrez des problèmes ou avez des questions ou des suggestions, n'hésitez pas à ouvrir un incident sur {githubLink}, ou demandez à la communauté sur {discordLink}.", - "in_app_manual_explained": "Nous vous encourageons à consulter le manuel intégré à l'application, accessible à partir de l'icône dans le coin supérieur droit de l'écran qui ressemble à ceci : {icône}", - "next_config_step_one": "Vous serez redirigé vers la page de configuration suivante. Cette page vous permettra de personnaliser les fichiers à inclure et à exclure, de définir un nom d'utilisateur et un mot de passe pour protéger votre système, et tout un tas d'autres options.", - "next_config_step_two": "Lorsque vous êtes satisfait de ces paramètres, vous pouvez commencer à scanner votre contenu dans Stash en cliquant sur {localized_task}, puis sur {localized_scan}.", + "in_app_manual_explained": "Nous vous encourageons à consulter le manuel de l'application, accessible à partir de l'icône située dans le coin supérieur droit de l'écran, qui ressemble à ceci : {icon}", + "next_config_step_one": "Vous serez ensuite dirigé vers la page de configuration suivante. Cette page vous permettra de personnaliser les fichiers à inclure et à exclure, de définir un nom d'utilisateur et un mot de passe pour protéger votre système, ainsi qu'un grand nombre d'autres options.", + "next_config_step_two": "Lorsque vous êtes satisfait de ces paramètres, vous pouvez commencer à analyser votre contenu dans Stash en cliquant sur {localized_task}, puis sur {localized_scan}.", "open_collective": "Consultez notre {open_collective_link} pour voir comment vous pouvez contribuer au développement continu de Stash.", "support_us": "Soutenez-nous", - "thanks_for_trying_stash": "Merci d'avoir utilisé Stash !", - "welcome_contrib": "Nous accueillons également les contributions sous forme de code (corrections de bogues, améliorations et nouvelles fonctionnalités), de tests, de rapports de bogues, de demandes d'améliorations et de fonctionnalités, et d'assistance aux autres utilisateurs. Les détails sont disponibles dans la section Contribution du manuel intégré à l'application.", - "your_system_has_been_created": "Succès ! Votre système a été créé !" + "thanks_for_trying_stash": "Merci d'avoir essayé Stash !", + "welcome_contrib": "Nous accueillons également les contributions sous forme de code (corrections de bogues, améliorations et nouvelles fonctionnalités), de tests, de rapports de bogues, de demandes d'améliorations et de fonctionnalités, et d'assistance utilisateurs. Vous trouverez plus de précisions dans la section \"Contribution\" du manuel de l'application.", + "your_system_has_been_created": "Bravo ! Votre système a été créé !" }, "welcome": { - "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail actuel, et s'il ne le trouve pas là, il revient à $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash soit lu à partir d'un fichier de configuration spécifique en l'exécutant avec les options -c ou --config .", + "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail courant, et s'il ne le trouve pas, il se reporte sur $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise un fichier de configuration spécifique en le lançant avec les options -c ou --config .", "in_current_stash_directory": "Dans le répertoire $HOME/.stash", - "in_the_current_working_directory": "Dans le répertoire de travail actuel", - "next_step": "Si vous êtes prêt à créer un nouvel environnement, veuillez sélectionner l'emplacement où vous souhaitez enregistrer votre fichier de configuration et cliquez sur Suivant.", - "store_stash_config": "Où voulez-vous stocker votre configuration Stash ?", - "unable_to_locate_config": "Si vous lisez ceci, Stash n'a pas pu trouver de configuration existante. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration.", - "unexpected_explained": "Si vous obtenez cet écran de façon inattendue, veuillez essayer de redémarrer Stash dans le bon répertoire de travail ou avec l'indicateur -c." + "in_the_current_working_directory": "Dans le répertoire de travail courant", + "next_step": "Une fois que tout est réglé, si vous êtes prêt à procéder à la configuration d'un nouveau système, choisissez l'emplacement où vous souhaitez stocker votre fichier de configuration et cliquez sur Suivant.", + "store_stash_config": "Où voulez-vous stocker votre configuration de Stash ?", + "unable_to_locate_config": "Si vous lisez ceci, Stash n'a pas pu trouver de configuration existante. Cet assistant vous guidera dans le processus de paramétrage d'une nouvelle configuration.", + "unexpected_explained": "Si vous obtenez cet écran de manière inattendue, essayez de redémarrer Stash dans le bon répertoire de travail ou avec le drapeau -c." }, "welcome_specific_config": { - "config_path": "Stash utilisera le chemin suivant pour le fichier de configuration : {path} ", + "config_path": "Stash utilisera le chemin suivant pour le fichier de configuration : {path}", "next_step": "Lorsque vous êtes prêt à procéder à la configuration d'un nouveau système, cliquez sur Suivant.", - "unable_to_locate_specified_config": "Si vous lisez ceci, alors Stash n'a pas pu trouver le fichier de configuration spécifié sur la ligne de commande ou l'environnement. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration." + "unable_to_locate_specified_config": "Si vous lisez ceci, Stash n'a pas pu trouver le fichier de configuration spécifié en ligne de commande ou dans l'environnement. Cet assistant vous guidera dans le processus de paramétrage d'une nouvelle configuration." }, - "welcome_to_stash": "Bienvenue sur Stash", - "folder": { - "up_dir": "Remonter d'un répertoire" - } + "welcome_to_stash": "Bienvenue sur Stash" }, - "stash_id": "Stash ID", - "stash_ids": "Stash IDs", + "stash_id": "ID Stash", + "stash_ids": "IDs Stash", + "stashbox": { + "go_review_draft": "Allez à {endpoint_name} pour revoir le document.", + "selected_stash_box": "Point de connexion Stash-Box sélectionné", + "submission_failed": "Envoi échoué", + "submission_successful": "Envoi réussi", + "submit_update": "Existe déjà dans {endpoint_name}" + }, + "statistics": "Statistiques", "stats": { - "image_size": "Taille Images", - "scenes_duration": "Durée Vidéos", - "scenes_size": "Taille Vidéos" + "image_size": "Taille des images", + "scenes_duration": "Durée des scènes", + "scenes_size": "Taille des scènes" }, - "status": "Statut : {statusText}", + "status": "Statut : {statusText}", "studio": "Studio", - "studio_depth": "Niveaux (empty for all)", + "studio_depth": "Niveaux (vides pour tous)", "studios": "Studios", - "sub_tag_count": "Nombre de sous-Tags", - "sub_tag_of": "Sous-Tag de {parent}", - "sub_tags": "Sous-Tags", - "subsidiary_studios": "Studios affiliés/filiales", - "synopsis": "Synopsis", - "tag": "Tag", - "tag_count": "Nombre de Tags", - "tags": "Tags", + "sub_tag_count": "Nombre de sous-étiquettes", + "sub_tag_of": "Sous-étiquette de {parent}", + "sub_tags": "Sous-étiquettes", + "subsidiary_studios": "Studios affiliés", + "synopsis": "Résumé", + "tag": "Étiquette", + "tag_count": "Nombre d'étiquettes", + "tags": "Étiquettes", "tattoos": "Tatouages", "title": "Titre", "toast": { "added_entity": "{entity} ajouté(e)", - "added_generation_job_to_queue": "Tâche de génération ajoutée dans la file des tâches", + "added_generation_job_to_queue": "Ajout d'une tâche de génération en file d'attente", "created_entity": "Créé(e) {entity}", - "default_filter_set": "Filtre par défaut enregistré", - "delete_entity": "Suppression de {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "default_filter_set": "Filtre par défaut défini", + "delete_entity": "Supprimer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} supprimé(e)(s)", "generating_screenshot": "Génération de la capture d'écran…", - "merged_tags": "Tags fusionnés", - "rescanning_entity": "Rescan de {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", + "merged_tags": "Étiquettes fusionnées", + "rescanning_entity": "Réexamen de {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "{entity} sauvegardé(e)", - "started_auto_tagging": "Démarrage du Taggage automatique", - "started_generating": "Génération de fichiers multimédia démarrée", - "started_importing": "Importation commencée", + "started_auto_tagging": "Démarrage de l'étiquetage automatique", + "started_generating": "Démarrage de la génération", + "started_importing": "Démarrage de l'importation", "updated_entity": "{entity} mis(e) à jour" }, "total": "Total", "true": "Vrai", "twitter": "Twitter", - "updated_at": "Date de modification", + "type": "Type", + "updated_at": "Actualisé le", "url": "URL", "videos": "Vidéos", + "view_all": "Voir tout", "weight": "Poids", "years_old": "ans" } diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json new file mode 100644 index 000000000..476be5ae9 --- /dev/null +++ b/ui/v2.5/src/locales/hu-HU.json @@ -0,0 +1,539 @@ +{ + "actions": { + "add": "Hozzáadás", + "add_directory": "Mappa Hozzáadása", + "add_entity": "{entityType} Hozzáadása", + "add_to_entity": "{entityType}hoz Adás", + "allow": "Engedélyez", + "allow_temporarily": "Időszakosan Engedélyez", + "apply": "Alkalmaz", + "auto_tag": "Automatikus Címkézés", + "backup": "Biztonsági Mentés", + "browse_for_image": "Kép tallózása…", + "cancel": "Mégsem", + "clean": "Tisztítás", + "clear": "Törlés", + "clear_back_image": "Hátsó kép törlése", + "clear_front_image": "Első kép törlése", + "clear_image": "Kép Törlése", + "close": "Bezár", + "confirm": "Jóváhagyás", + "continue": "Folytatás", + "create": "Létrehoz", + "create_entity": "{entityType} Létrehozása", + "delete": "Törlés", + "delete_entity": "{entityType} Törlése", + "delete_file": "Fájl Törlése", + "delete_generated_supporting_files": "Létrehozott kiegészítő fájlok törlése", + "disallow": "Tiltás", + "download": "Letöltés", + "download_backup": "Biztonsági Mentés Letöltése", + "edit": "Módosít", + "edit_entity": "{entityType} Módosítása", + "export": "Exportálás…", + "export_all": "Összes exportálása…", + "find": "Keresés", + "finish": "Befejez", + "from_file": "Fáljból…", + "from_url": "URL-ből…", + "full_export": "Teljes Export", + "full_import": "Teljes Import", + "generate": "Generálás", + "generate_thumb_default": "Alapértelmezett bélyegkép generálása", + "generate_thumb_from_current": "Bélyegkép generálása a jelenlegiből", + "hide": "Elrejtés", + "hide_configuration": "Beállítások Elrejtése", + "identify": "Beazonosítás", + "ignore": "Mellőz", + "import": "Importálás…", + "import_from_file": "Importálás fájlból", + "logout": "Kijelentkezés", + "merge": "Egyesítés", + "next_action": "Következő", + "not_running": "nem fut", + "open_random": "Véletlenszerű Megnyitása", + "overwrite": "Felülír", + "play_random": "Véletlenszerű Lejátszása", + "play_selected": "Kiválasztott Lejátszása", + "preview": "Előnézet", + "previous_action": "Vissza", + "refresh": "Frissítés", + "reload_plugins": "Pluginek újratöltése", + "reload_scrapers": "Scrapperek újratöltése", + "remove": "Eltávolítás", + "rename_gen_files": "Generált fájlok átnevezése", + "rescan": "Újra Szkennelés", + "reshuffle": "Újrakeverés", + "running": "fut", + "save": "Mentés", + "save_filter": "Szűrő mentése", + "scan": "Szkennelés", + "search": "Keresés", + "select_all": "Mind Kijelölése", + "select_entity": "{entityType} Kijelölése", + "select_folders": "Mappák kijelölése", + "select_none": "Kijelölés Törlése", + "set_as_default": "Beállítás alapértelmezettként", + "set_image": "Kép beállítása…", + "show": "Megjelenít", + "show_configuration": "Beállítások Megjelenítése", + "skip": "Kihagyás", + "stop": "Megállít", + "submit": "Beküldés", + "submit_stash_box": "Stash-Box-ba Beküldés", + "tasks": { + "clean_confirm_message": "Biztos vagy benne hogy el akarod végezni a Tisztítást? Ez a művelet le fogja törölni a fáljrendszerben már nem megtalálható összes jelenet és galéria adatbázis információit és generált tartalmait.", + "import_warning": "Biztos vagy benne, hogy importálni akarsz? Ez a művelet le fogja törölni az adatbázist és újraimportálja az kiexportált metaadatok alapján." + }, + "use_default": "Alapértelmezett használata" + }, + "age": "Kor", + "aliases": "Álnevek", + "all": "mind", + "ascending": "Növekvő", + "average_resolution": "Átlagos Felbontás", + "birth_year": "Születési Év", + "birthdate": "Születési Dátum", + "bitrate": "Bitráta", + "career_length": "Karier Hossza", + "component_tagger": { + "config": { + "blacklist_label": "Tiltólista", + "query_mode_auto": "Automatikus", + "query_mode_dir": "Mappa", + "query_mode_filename": "Fájlnév", + "query_mode_metadata": "Metaadat", + "query_mode_path": "Elérési út", + "source": "Forrás" + }, + "results": { + "duration_unknown": "Ismeretlen hossz", + "unnamed": "Névtelen" + }, + "verb_matched": "Egyezik" + }, + "config": { + "about": { + "latest_version": "Legfrissebb Verzió", + "version": "Verzió" + }, + "categories": { + "logs": "Logok", + "metadata_providers": "Metaadat Szolgáltatók", + "plugins": "Pluginek", + "security": "Biztonság", + "services": "Szolgáltatások", + "system": "Rendszer", + "tasks": "Feladatok", + "tools": "Eszközök" + }, + "dlna": { + "allow_temp_ip": "{tempIP} Engedélyezése", + "allowed_ip_addresses": "Engedélyezett IP-címek", + "default_ip_whitelist": "Alapértelmezett IP Engedélyezőlista", + "disallowed_ip": "Letiltott IP", + "enabled_by_default": "Alapértelmezetten Engedélyezve", + "network_interfaces": "Kezelőfelületek", + "recent_ip_addresses": "Legutóbbi IP-címek", + "server_display_name": "Szerver Megjelenített Neve", + "until_restart": "újraindításig" + }, + "general": { + "auth": { + "api_key": "API Kulcs", + "authentication": "Hitelesítés", + "clear_api_key": "API kulcs törlése", + "generate_api_key": "API kulcs generálása", + "log_file": "Log fájl", + "password": "Jelszó", + "username": "Felhasználónév" + }, + "calculate_md5_and_ohash_label": "MD5 kiszámítása a videókhoz", + "chrome_cdp_path": "Chrome CDP elérési út", + "create_galleries_from_folders_desc": "Igaz esetén galériákat készít a képeket tartalmazó mappákból.", + "create_galleries_from_folders_label": "Galériák készítése a képeket tartalmazó mappákból", + "db_path_head": "Adatbázis Elérési Út", + "image_ext_desc": "Képként értelmezendő fájlkiterjesztések vesszővel elválasztott listája.", + "include_audio_desc": "Hozzáadja a hangsávot a generált bemutatókhoz.", + "include_audio_head": "Hangsáv hozzáadása", + "logging": "Logolás", + "metadata_path": { + "heading": "Metaadatok Elérési Útja" + }, + "preview_generation": "Bemutató Generálás", + "python_path": { + "heading": "Python Elérési Út" + }, + "sqlite_location": "SQLite adatbázis-fájl elérési útja (újraindítás szükséges)", + "video_ext_desc": "Videóként értelmezendő fájlkiterjesztések vesszővel elválasztott listája.", + "video_head": "Videó" + }, + "library": { + "exclusions": "Kivételek", + "gallery_and_image_options": "Galéria és Kép beállítások" + }, + "logs": { + "log_level": "Logolás Szintje" + }, + "scraping": { + "entity_metadata": "{entityType} Metaadatok", + "search_by_name": "Keresés név szerint", + "supported_types": "Támogatott típusok", + "supported_urls": "URL-ek" + }, + "stashbox": { + "api_key": "API kulcs", + "name": "Név" + }, + "tasks": { + "added_job_to_queue": "{operation_name} hozzáadva a feladatlistához", + "backing_up_database": "Adatbázis biztonsági mentése folyamatban", + "backup_and_download": "Biztonsági mentést hajt végre az adatbázison és letölti a fájlt.", + "cleanup_desc": "Ellenőrzi hogy hiányoznak-e fájlok, és eltávolítja őket az adatbázisból. Ez egy visszavonhatatlan művelet.", + "data_management": "Adatkezelés", + "defaults_set": "Az alapértelmezett értékek be lettek állítva, és ezek lesznek használva a Feladatok oldalon a {action} gomb megnyomásakor.", + "dont_include_file_extension_as_part_of_the_title": "Ne csatolja a fájlkiterjesztést a címhez", + "empty_queue": "Jelenleg nem fut feladat.", + "generate_thumbnails_during_scan": "Bélyegképek generálása a képekhez", + "generate_video_previews_during_scan": "Bemutatók generálása", + "generated_content": "Legenerált Tartalom", + "identify": { + "default_options": "Alapértelmezett Beállítások", + "field": "Mező", + "field_behaviour": "{strategy} {field}", + "field_options": "Mező Beállítások", + "heading": "Azonosítás", + "identifying_scenes": "{num} {scene} azonosítása", + "set_cover_images": "Borítoképek beállítása", + "source": "Forrás", + "source_options": "{source} Beállítás", + "sources": "Források", + "strategy": "Stratégia" + }, + "job_queue": "Feladatlista", + "maintenance": "Karbantartás", + "scan": { + "scanning_all_paths": "Összes elérési út szkennelése" + } + }, + "tools": { + "scene_duplicate_checker": "Dupla Jelenet Ellenőrző", + "scene_filename_parser": { + "add_field": "Mező Hozzáadása", + "capitalize_title": "Nagybetűs cím", + "display_fields": "Megjelenített mezők", + "filename": "Fájlnév", + "filename_pattern": "Fájlnév Minta", + "ignored_words": "Figyelmen kívül hagyott szavak" + }, + "scene_tools": "Jelenet Eszközök" + }, + "ui": { + "basic_settings": "Alapvető Beállítások", + "custom_css": { + "description": "Az oldalt újra be kell tölteni hogy a változtatások életbe lépjenek.", + "heading": "Egyéni CSS", + "option_label": "Egyéni CSS engedélyezve" + }, + "delete_options": { + "description": "Alapértelmezett beállítások képek, galériák és jelenetek törlése esetén.", + "heading": "Törlési Beállítások", + "options": { + "delete_file": "Fájl törlése alapértelmezettként", + "delete_generated_supporting_files": "Generált kiegészítő fájlok törlése alapértelmezettként" + } + }, + "desktop_integration": { + "notifications_enabled": "Értesítések Engedélyezése" + }, + "editing": { + "heading": "Szerkesztés" + }, + "handy_connection": { + "connect": "Csatlakozás" + }, + "images": { + "heading": "Képek" + }, + "interactive_options": "Interaktív Beállítások", + "language": { + "heading": "Nyelv" + }, + "preview_type": { + "options": { + "animated": "Mozgókép", + "static": "Állókép", + "video": "Videó" + } + }, + "scene_list": { + "heading": "Jelenetlista" + }, + "scene_player": { + "heading": "Jelenet-lejátszó", + "options": { + "auto_start_video": "Automatikus videóindítás" + } + }, + "scene_wall": { + "options": { + "toggle_sound": "Hang engedélyezése" + } + }, + "title": "Felhasználói Felület" + } + }, + "configuration": "Beállítások", + "country": "Ország", + "cover_image": "Borítókép", + "created_at": "Létrehozva", + "criterion": { + "greater_than": "Nagyobb mint", + "less_than": "Kisebb mint", + "value": "Érték" + }, + "criterion_modifier": { + "between": "között", + "equals": "egyenlő", + "excludes": "kivéve", + "includes": "beleértve", + "includes_all": "mindet belevéve", + "is_null": "egyenlő null", + "matches_regex": "megfelel regex-nek", + "not_between": "nincs közte", + "not_equals": "nem egyenlő" + }, + "custom": "Egyéni", + "date": "Dátum", + "death_date": "Halál Dátuma", + "death_year": "Halál Éve", + "descending": "Csökkenő", + "detail": "Részlet", + "details": "Részletek", + "developmentVersion": "Fejlesztői Verzió", + "dialogs": { + "delete_object_desc": "Biztos hogy törölni akarod {count, plural, one {ezt a {singularEntity}} ezeket a {these {pluralEntity}}}?", + "export_title": "Exportálás", + "lightbox": { + "display_mode": { + "original": "Eredeti" + }, + "options": "Beállítások", + "scroll_mode": { + "zoom": "Nagyítás" + } + }, + "merge_tags": { + "destination": "Cél", + "source": "Forrás" + }, + "scene_gen": { + "preview_exclude_end_time_desc": "Az utolsó x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva.", + "preview_exclude_start_time_desc": "Az első x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva." + }, + "scrape_results_existing": "Létező", + "set_image_url_title": "Kép URL" + }, + "director": "Rendező", + "display_mode": { + "grid": "Háló", + "list": "Lista", + "unknown": "Ismeretlen", + "wall": "Fal" + }, + "donate": "Adomány", + "dupe_check": { + "description": "'Pontos' alatti szintek kiszámítása tovább tarthat. Hibás találatok is megjelenhetnek alacsonyabb pontossági szinteken.", + "options": { + "exact": "Pontos", + "high": "Magas", + "low": "Alacsony", + "medium": "Közepes" + }, + "search_accuracy_label": "Keresési Pontosság", + "title": "Megkettőzött Jelenetek" + }, + "duplicated_phash": "Megkettőzőtt (phash)", + "duration": "Hossz", + "effect_filters": { + "blue": "Kék", + "blur": "Elmosás", + "brightness": "Fényerő", + "contrast": "Kontraszt", + "gamma": "Gamma", + "green": "Zöld", + "hue": "Árnyalat", + "name": "Szűrők", + "red": "Piros", + "reset_filters": "Szűrők Törlése", + "rotate": "Forgat", + "saturation": "Szaturáció", + "scale": "Méretarány", + "warmth": "Melegség" + }, + "ethnicity": "Etnikum", + "eye_color": "Szemszín", + "fake_tits": "Szilikonmellek", + "false": "Hamis", + "favourite": "Kedvenc", + "file": "fájl", + "file_info": "Fájl Információ", + "files": "fájlok", + "filesize": "Fájl Méret", + "filter": "Szűrő", + "filters": "Szűrők", + "galleries": "Galériák", + "gallery": "Galéria", + "gallery_count": "Galéria Száma", + "gender": "Nem", + "gender_types": { + "FEMALE": "Nő", + "INTERSEX": "Nemek közti", + "MALE": "Férfi" + }, + "hair_color": "Hajszín", + "handy_connection_status": { + "connecting": "Kapcsolódás", + "disconnected": "Szétkapcsolt", + "missing": "Hiányzó", + "ready": "Kész", + "uploading": "Szkript feltöltése" + }, + "height": "Magasság", + "help": "Segítség", + "image": "Kép", + "image_count": "Képek Száma", + "images": "Képek", + "instagram": "Instagram", + "interactive": "Interaktív", + "interactive_speed": "Interaktív sebesség", + "isMissing": "Hiányzik", + "library": "Könyvtár", + "loading": { + "generic": "Betöltés…" + }, + "media_info": { + "downloaded_from": "Letöltés Forrása", + "interactive_speed": "Interaktív sebesség", + "performer_card": { + "age": "{age} {years_old}" + } + }, + "metadata": "Metaadatok", + "movie": "Film", + "movies": "Filmek", + "name": "Név", + "new": "Új", + "none": "Nincs", + "o_counter": "O-Számláló", + "operations": "Műveletek", + "organized": "Rendezve", + "pagination": { + "first": "Első", + "last": "Utolsó", + "next": "Következő", + "previous": "Előző" + }, + "parent_tags": "Szülő-címkék", + "path": "Elérési Út", + "performer": "Szereplő", + "performerTags": "Szereplő Címkék", + "performer_age": "Szereplő Kora", + "performer_count": "Szereplők Száma", + "performer_favorite": "Szereplő Kedvencek Közt", + "performer_image": "Szereplő Képe", + "performer_tagger": { + "config": { + "excluded_fields": "Kihagyott mezők:" + }, + "current_page": "Jelenlegi oldal", + "network_error": "Hálózati Hiba", + "tag_status": "Címke Státusza", + "update_performer": "Szereplő Frissítése", + "update_performers": "Szereplők Frissítése" + }, + "performers": "Szereplők", + "piercings": "Piercingek", + "queue": "Sor", + "random": "Véletlenszerű", + "rating": "Értékelés", + "resolution": "Felbontás", + "scene": "Jelenet", + "sceneTagger": "Jelenetcímkéző", + "sceneTags": "Jelenetcímkék", + "scene_count": "Jelenetszám", + "scene_id": "Jelenet ID", + "scenes": "Jelenetek", + "search_filter": { + "add_filter": "Szűrő Hozzáadása", + "name": "Szűrő", + "saved_filters": "Mentett szűrők", + "update_filter": "Szűrő Frissítése" + }, + "seconds": "Másodperc", + "settings": "Beállítások", + "setup": { + "confirm": { + "nearly_there": "Már majdnem kész!" + }, + "errors": { + "something_went_wrong_while_setting_up_your_system": "Valami hiba történt a rendszer beállításakor. Itt a hibaüzenet: {error}" + }, + "folder": { + "file_path": "Fájl elérési út" + }, + "migrate": { + "backup_recommended": "Ajánlott az adatbázis biztonsági mentése az áttelepítés előtt.Meg tudjuk ezt tenni neked az adatbázis átmásolásával a {defaultBackupPath} címre.", + "migrating_database": "Adatbázis áttelepítése", + "migration_failed": "Sikertelen áttelepítés", + "migration_irreversible_warning": "A séma áttelepítése nem visszafordítható folyamat. Amint az áttelepítés elkezdődik, az adatbázis összeegyeztethetetlen lesz a Stash előző verzióival.", + "migration_required": "Áttelepítés szükséges", + "schema_too_old": "A jelenlegi Stash adatbázis verziója {databaseSchema} , amit át kell telepíteni {appSchema} verzióra. A Stash ezen verziója nem fog működni az adatbázis áttelepítése nélkül." + }, + "success": { + "help_links": "Ha problémába ütközöl, kérdésed, vagy javaslatod van, nyugodtan jelezd {githubLink}, vagy kérdezd meg a közösségtől {discordLink}.", + "support_us": "Támogatás" + }, + "welcome": { + "config_path_logic_explained": "A Stash elöszőr a jelenlegi munkakönyvtárban próbálja a konfigurációs fájlját (config.yml) megkeresni. Ha ott nem találja, akkor a következő helyen próbálkozik: $HOME/.stash/config.yml (Windows rendszeren: %USERPROFILE%\\.stash\\config.yml). Meghatározhatja hogy a Stash egy bizonyos konfigurációs fájlt használjon, ammennyiben a -c or --config paraméterekkel indítja.", + "unexpected_explained": "Amennyiben ez a képernyő váratlanul bukkant fel, próbáld újraindítani a Stasht a megfelelő munkakönyvtárban, vagy a -c flag-gel." + }, + "welcome_specific_config": { + "unable_to_locate_specified_config": "Ha ezt olvasod, akkor a Stash nem találta meg a konfigurációs fájlt, amit megadtál a parancssorban. Ez a varázsló végigvezet a lépéseken, hogy új konfigurációs fájlt tudj beállítani." + } + }, + "stashbox": { + "submission_failed": "Beküldés sikertelen", + "submission_successful": "Beküldés sikeres" + }, + "statistics": "Statisztikák", + "stats": { + "image_size": "Képek mérete", + "scenes_duration": "Jelenetek hossza", + "scenes_size": "Jelenetek mérete" + }, + "status": "Státusz: {statusText}", + "studio": "Stúdió", + "studios": "Stúdiók", + "tag": "Címke", + "tag_count": "Címkék Száma", + "tags": "Címkék", + "tattoos": "Tetoválások", + "title": "Cím", + "toast": { + "added_entity": "{entity} Hozzáadva", + "created_entity": "{entity} Létrehozva", + "merged_tags": "Összevont címkék", + "saved_entity": "{entity} Mentve", + "updated_entity": "{entity} Frissítve" + }, + "total": "Összesen", + "true": "Igaz", + "twitter": "Twitter", + "updated_at": "Frissítés Ideje", + "url": "URL", + "videos": "Videók", + "view_all": "Mindegyik Megjelenítése", + "weight": "Súly", + "years_old": "éves" +} diff --git a/ui/v2.5/src/locales/index.ts b/ui/v2.5/src/locales/index.ts index 0b03b1a64..bf6fd19b4 100644 --- a/ui/v2.5/src/locales/index.ts +++ b/ui/v2.5/src/locales/index.ts @@ -1,41 +1,24 @@ -import deDE from "./de-DE.json"; -import enGB from "./en-GB.json"; -import enUS from "./en-US.json"; -import esES from "./es-ES.json"; -import ptBR from "./pt-BR.json"; -import frFR from "./fr-FR.json"; -import itIT from "./it-IT.json"; -import fiFI from "./fi-FI.json"; -import svSE from "./sv-SE.json"; -import zhTW from "./zh-TW.json"; -import zhCN from "./zh-CN.json"; -import hrHR from "./hr-HR.json"; -import nlNL from "./nl-NL.json"; -import ruRU from "./ru-RU.json"; -import trTR from "./tr-TR.json"; -import jaJP from "./ja-JP.json"; -import plPL from "./pl-PL.json"; -import daDK from "./da-DK.json"; -import koKR from "./ko-KR.json"; +export const localeLoader = { + deDE: () => import("./de-DE.json"), + enGB: () => import("./en-GB.json"), + enUS: () => import("./en-US.json"), + esES: () => import("./es-ES.json"), + ptBR: () => import("./pt-BR.json"), + frFR: () => import("./fr-FR.json"), + itIT: () => import("./it-IT.json"), + fiFI: () => import("./fi-FI.json"), + svSE: () => import("./sv-SE.json"), + zhTW: () => import("./zh-TW.json"), + zhCN: () => import("./zh-CN.json"), + hrHR: () => import("./hr-HR.json"), + nlNL: () => import("./nl-NL.json"), + ruRU: () => import("./ru-RU.json"), + trTR: () => import("./tr-TR.json"), + jaJP: () => import("./ja-JP.json"), + plPL: () => import("./pl-PL.json"), + daDK: () => import("./da-DK.json"), + koKR: () => import("./ko-KR.json"), + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as { [key: string]: any }; -export default { - deDE, - enGB, - enUS, - esES, - ptBR, - frFR, - itIT, - fiFI, - svSE, - zhTW, - zhCN, - hrHR, - nlNL, - ruRU, - trTR, - jaJP, - plPL, - daDK, - koKR, -}; +export default localeLoader; diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 3dc92a1f7..a2e2286f2 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -23,6 +23,7 @@ "create_entity": "Crea {entityType}", "create_marker": "Crea Marker", "created_entity": "Creata/o {entity_type}: {entity_name}", + "customise": "Personalizza", "delete": "Cancella", "delete_entity": "Cancella {entityType}", "delete_file": "Cancella file", @@ -100,6 +101,7 @@ "stop": "Stop", "submit": "Invia", "submit_stash_box": "Invia a Stash-Box", + "submit_update": "Invia aggiornamento", "tasks": { "clean_confirm_message": "Sei sicuro di voler Pulire? Questa azione cancellerà informazioni e contenuto creato dal database per tutte le scene e gallerie che non si trovano più nel file system.", "dry_mode_selected": "Dry Mode selezionato. Nessuna cancellazione avverrà, solo log.", @@ -733,6 +735,12 @@ "filters": "Filtri", "framerate": "Frequenza dei Fotogrammi", "frames_per_second": "{value} fotogrammi per secondo", + "front_page": { + "types": { + "premade_filter": "Filtro Già Creato", + "saved_filter": "Filtro Salvato" + } + }, "galleries": "Gallerie", "gallery": "Galleria", "gallery_count": "Numero Gallerie", @@ -860,11 +868,8 @@ "queue": "Coda", "random": "Casuale", "rating": "Classif.", - "recently_added_performers": "Attori Aggiunti Recentemente", - "recently_added_studios": "Studio Aggiunti Recentemente", - "recently_released_galleries": "Gallerie Distribuite Recentemente", - "recently_released_movies": "Film Distribuiti Recentemente", - "recently_released_scenes": "Video Distribuiti Recentemente", + "recently_added_objects": "{objects} Aggiunto Recentemente", + "recently_released_objects": "{objects} Recentemente Distribuito", "resolution": "Risoluzione", "scene": "Scena", "sceneTagger": "Tagger Scena", @@ -966,7 +971,8 @@ "go_review_draft": "Vai al {endpoint_name} per revisionare la bozza.", "selected_stash_box": "Endpoint Stash-Box selezionato", "submission_failed": "Invio fallito", - "submission_successful": "Invio riuscito" + "submission_successful": "Invio riuscito", + "submit_update": "Già esistente in {endpoint_name}" }, "statistics": "Statistiche", "stats": { @@ -1007,6 +1013,7 @@ "total": "Totale", "true": "Vero", "twitter": "Twitter", + "type": "Tipo", "updated_at": "Aggiornato Al", "url": "URL", "videos": "Video", diff --git a/ui/v2.5/src/locales/ja-JP.json b/ui/v2.5/src/locales/ja-JP.json index 2210a6621..1d1d5d08c 100644 --- a/ui/v2.5/src/locales/ja-JP.json +++ b/ui/v2.5/src/locales/ja-JP.json @@ -23,6 +23,7 @@ "create_entity": "{entityType}を作成", "create_marker": "マーカーを作成", "created_entity": "{entity_type}を作成しました: {entity_name}", + "customise": "カスタマイズ", "delete": "削除", "delete_entity": "{entityType}を削除", "delete_file": "ファイルを削除", @@ -100,6 +101,7 @@ "stop": "停止", "submit": "送信", "submit_stash_box": "Stash-Boxに送信", + "submit_update": "更新を送信", "tasks": { "clean_confirm_message": "クリーニングを実行してもよろしいですか?この操作により、ファイルシステムで利用されていないすべてのシーンとギャラリーから生成されたコンテンツとデータベース情報が削除されます。", "dry_mode_selected": "ドライモードが選択されています。実際の削除は実施されず、ログ処理だけが実行されます。", @@ -733,6 +735,12 @@ "filters": "フィルター", "framerate": "フレームレート", "frames_per_second": "{value}FPS", + "front_page": { + "types": { + "premade_filter": "既製フィルター", + "saved_filter": "保存済みフィルター" + } + }, "galleries": "ギャラリー", "gallery": "ギャラリー", "gallery_count": "ギャラリー数", @@ -860,11 +868,8 @@ "queue": "キュー", "random": "ランダム", "rating": "評価", - "recently_added_performers": "最近追加された出演者", - "recently_added_studios": "最近追加されたスタジオ", - "recently_released_galleries": "最近リリースされたギャラリー", - "recently_released_movies": "最近リリースされた映画", - "recently_released_scenes": "最近リリースされたシーン", + "recently_added_objects": "最近追加された{objects}", + "recently_released_objects": "最近リリースされた{objects}", "resolution": "解像度", "scene": "シーン", "sceneTagger": "シーン一括タグ付け", @@ -966,7 +971,8 @@ "go_review_draft": "下書きを確認するには、{endpoint_name}に移動してください。", "selected_stash_box": "選択済みのStash-Boxエンドポイント", "submission_failed": "送信に失敗しました", - "submission_successful": "送信完了しました" + "submission_successful": "送信完了しました", + "submit_update": "{endpoint_name}に既に存在します" }, "statistics": "統計", "stats": { @@ -1007,6 +1013,7 @@ "total": "合計", "true": "有効", "twitter": "Twitter", + "type": "タイプ", "updated_at": "更新日:", "url": "URL", "videos": "動画", diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index f37512f69..14b4f5910 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -23,6 +23,7 @@ "create_entity": "{entityType} 생성", "create_marker": "마커 생성", "created_entity": "{entity_type}을 생성했습니다. ({entity_name})", + "customise": "사용자 정의", "delete": "삭제", "delete_entity": "{entityType} 삭제", "delete_file": "파일 삭제", @@ -45,13 +46,17 @@ "generate": "만들기", "generate_thumb_default": "기본 썸네일 만들기", "generate_thumb_from_current": "현재 화면으로 썸네일 만들기", + "hash_migration": "해쉬 값 마이그레이션", "hide": "숨기기", "hide_configuration": "설정 숨기기", + "identify": "인증", "ignore": "무시", "import": "불러오기…", "import_from_file": "파일 불러오기", "logout": "로그아웃", "merge": "합치기", + "merge_from": "...에서 합치기", + "merge_into": "...로 합치기", "next_action": "다음", "not_running": "실행 중이 아님", "open_in_external_player": "외부 플레이어에서 열기", @@ -75,11 +80,17 @@ "save_filter": "필터 저장", "scan": "스캔", "scrape": "스크레이핑하기", - "scrape_query": "스크레이핑 쿼리", + "scrape_query": "쿼리 스크레이핑하기", + "scrape_scene_fragment": "단편적 스크레이핑하기", + "scrape_with": "스크레이핑하기…", "search": "검색", "select_all": "모두 검색", "select_entity": "{entityType} 선택", "select_folders": "폴더 선택", + "select_none": "선택하지 않음", + "selective_auto_tag": "선택적 자동 태깅", + "selective_clean": "선택적 데이터베이스 정리", + "selective_scan": "선택적 스캔", "set_as_default": "기본값으로 설정", "set_back_image": "이전 사진…", "set_front_image": "처음 사진…", @@ -90,11 +101,15 @@ "stop": "정지", "submit": "제출", "submit_stash_box": "Stash-Box에 제출하기", + "submit_update": "업데이트 제출하기", "tasks": { + "clean_confirm_message": "정말로 데이터베이스 정리를 하시겠습니까? 파일 시스템에 존재하지 않는 파일의 데이터베이스 정보와 컨텐츠가 삭제될 것입니다.", + "dry_mode_selected": "삭제하지 않기 모드가 선택되었습니다. 삭제를 진행하지 않고, 로깅만 할 것입니다.", "import_warning": "정말 불러오기를 하시겠습니까? 데이터베이스를 삭제하고 불러온 메타데이터로 덮어쓰게 됩니다." }, "temp_disable": "임시 비활성화…", "temp_enable": "임시 활성화…", + "unset": "설정 해제", "use_default": "기본값 사용", "view_random": "랜덤 보기" }, @@ -106,7 +121,7 @@ "ascending": "오름차순", "average_resolution": "평균 해상도", "birth_year": "태어난 년도", - "birthdate": "생일", + "birthdate": "생년월일", "bitrate": "비트레이트", "captions": "자막", "career_length": "배우 경력", @@ -116,6 +131,7 @@ "blacklist_desc": "블랙리스트에 있는 아이템들은 쿼리에서 제외됩니다. (주의: 아이템들은 정규 표현식으로 적혀 있어야 하며 대소문자를 구별합니다. 다음 문자들의 앞에는 백슬래쉬(\\)를 넣어주어야 합니다: {chars_require_escape})", "blacklist_label": "블랙리스트", "query_mode_auto": "자동", + "query_mode_auto_desc": "메타데이터가 있다면 사용하고, 그렇지 않다면 파일 이름을 사용합니다", "query_mode_dir": "디렉토리", "query_mode_dir_desc": "비디오 파일의 상위 경로만 사용", "query_mode_filename": "파일 이름", @@ -125,24 +141,43 @@ "query_mode_metadata_desc": "메타데이터만 사용", "query_mode_path": "경로", "query_mode_path_desc": "전체 파일 경로 사용", + "set_cover_desc": "영상 커버가 있다면 그 이미지로 교체합니다.", "set_cover_label": "영상 커버 이미지 설정", + "set_tag_desc": "영상에 이미 존재하는 태그들을 덮어쓰기/합치기 함으로써 태그를 영상에 추가합니다.", "set_tag_label": "태그 설정", - "show_male_label": "남성 배우 보여주기" + "show_male_desc": "남성 배우들의 태그 가능 여부 설정을 켜거나 끕니다.", + "show_male_label": "남성 배우 보여주기", + "source": "출처" }, "noun_query": "쿼리", "results": { + "duration_off": "영상 길이가 최소 {number}초 차이남", + "duration_unknown": "영상 길이 알 수 없음", + "fp_found": "{fpCount, plural, =0 {일치하는 새로운 식별값을 찾지 못했습니다.} other {# 일치하는 새로운 식별값을 찾았습니다.}}", + "fp_matches": "영상 길이가 일치함", + "fp_matches_multi": "영상 길이가 {durationsLength}개 중 {matchCount}개의 식별값과 일치합니다", + "hash_matches": "{hash_type}이 일치함", "match_failed_already_tagged": "이미 태깅된 영상", "match_failed_no_result": "결과 없음", "match_success": "영상 태깅 성공", + "phash_matches": "{count}개의 PHash가 일치함", "unnamed": "이름 없음" }, - "verb_scrape_all": "모두 스크레이핑하기" + "verb_match_fp": "식별값 비교하기", + "verb_matched": "일치함", + "verb_scrape_all": "모두 스크레이핑하기", + "verb_submit_fp": "{fpCount, plural, one{# 식별값} other{# 식별값들}} 제출하기", + "verb_toggle_config": "{configuration} {toggle}", + "verb_toggle_unmatched": "일치하지 않는 영상 {toggle}" }, "config": { "about": { + "build_hash": "빌드 해쉬 값:", + "build_time": "빌드된 시간:", "check_for_new_version": "새로운 버전 체크", "latest_version": "최신 버전", "latest_version_build_hash": "최신 버전 빌드 해쉬:", + "new_version_notice": "[새 버전]", "stash_discord": "디스코드: {url}", "stash_home": "깃허브: {url}", "stash_open_collective": "후원: {url}", @@ -153,6 +188,7 @@ "heading": "앱 경로" }, "categories": { + "about": "프로그램 정보", "interface": "인터페이스", "logs": "로그", "metadata_providers": "메타데이터", @@ -167,17 +203,29 @@ "dlna": { "allow_temp_ip": "{tempIP} 허용", "allowed_ip_addresses": "허용된 IP 주소", + "allowed_ip_temporarily": "임시로 허용된 IP", "default_ip_whitelist": "IP 화이트리스트 기본값", + "default_ip_whitelist_desc": "기본 IP 주소들은 DLNA에 접근할 수 있습니다. 모든 IP 주소들을 허용하려면 {wildcard} 문자를 사용하세요.", + "disabled_dlna_temporarily": "임시로 DNLA를 비활성화했습니다", "disallowed_ip": "금지된 IP", + "enabled_by_default": "기본값으로 활성화됨", + "enabled_dlna_temporarily": "임시로 DLNA를 활성화함", "network_interfaces": "인터페이스", - "recent_ip_addresses": "최근 IP 주소" + "network_interfaces_desc": "DLNA 서버를 노출시키기 위한 인터페이스입니다. 빈 리스트로 두면 모든 인터페이스에서 작동하게 됩니다. 변경 후 DLNA를 재시작해야 합니다.", + "recent_ip_addresses": "최근 IP 주소", + "server_display_name": "서버 이름 (display name)", + "server_display_name_desc": "DLNA 서버를 위한 이름(display name)입니다. 빈 칸으로 두면 기본값으로 {server_name}를 사용합니다.", + "successfully_cancelled_temporary_behaviour": "임시 설정을 취소하는 데에 성공했습니다", + "until_restart": "재시작 전까지" }, "general": { "auth": { "api_key": "API 키", + "api_key_desc": "외부 시스템을 위한 API 키입니다. 아이디/비밀번호가 설정되었을 때에만 필요합니다. 아이디는 API 키 생성 전에 저장되어야만 합니다.", "authentication": "인증", "clear_api_key": "API 키 삭제", "credentials": { + "description": "Stash로의 접속을 제한하기 위한 자격 요건입니다.", "heading": "자격증명서" }, "generate_api_key": "API 키 생성", @@ -191,50 +239,83 @@ "maximum_session_age_desc": "사용되지 않을 때 자동으로 로그아웃되기까지의 시간입니다 (단위: 초).", "password": "비밀번호", "password_desc": "Stash에 접속하기 위한 비밀번호입니다. 로그인 과정을 생략하려면 빈 칸으로 두십시오", + "stash-box_integration": "Stash-box 통합", "username": "아이디", "username_desc": "Stash에 접속하기 위한 아이디입니다. 로그인을 생략하려면 빈 칸으로 두십시오" }, "cache_location": "캐시 폴더 경로", "cache_path_head": "캐쉬 경로", + "calculate_md5_and_ohash_desc": "oshash 외에 MD5 체크섬도 계산합니다. 활성화하면 초기 스캔을 더 느리게 만들 것입니다. MD5 계산을 사용하지 않으려면 파일 이름 해쉬를 oshash로 설정해야 합니다.", + "calculate_md5_and_ohash_label": "비디오 MD5 계산하기", + "check_for_insecure_certificates": "안전하지 않은 자격증명을 검사", + "check_for_insecure_certificates_desc": "일부 사이트에서는 안전하지 않은 SSL 인증서를 사용합니다. 스크레이퍼를 선택하지 않으면 안전하지 않은 인증서 검사를 건너뛰고 해당 사이트를 스크레이핑할 수 있습니다. 스크레이핑 시 인증서 오류가 발생하면 이 체크 표시를 해제하세요.", "chrome_cdp_path": "Chrome CDP 경로", + "chrome_cdp_path_desc": "Chrome 실행 파일의 경로, 또는 Chrome 인스턴스의 원격 주소입니다(http:// 또는 https://로 시작합니다. 예시: http://localhost:9222/json/version).", + "create_galleries_from_folders_desc": "체크하면, 이미지를 포함한 폴더들로부터 갤러리를 생성합니다.", "create_galleries_from_folders_label": "이미지가 들어있는 폴더로부터 갤러리 생성", "db_path_head": "데이터베이스 경로", + "directory_locations_to_your_content": "컨텐츠가 있는 폴더 위치", + "excluded_image_gallery_patterns_desc": "스캔과 데이터베이스 정리에서 제외할 이미지와 갤러리 파일/경로의 정규표현식", + "excluded_image_gallery_patterns_head": "제외된 이미지/갤러리 패턴", + "excluded_video_patterns_desc": "스캔과 데이터베이스 정리에서 제외할 비디오 파일/경로의 정규표현식", + "excluded_video_patterns_head": "제외된 비디오 패턴", + "gallery_ext_desc": "갤러리 zip 파일로 인식될 파일 확장자입니다 (쉼표로 구분합니다).", + "gallery_ext_head": "갤러리 zip 확장자", + "generated_file_naming_hash_desc": "생성된 파일 이름을 정할 때 MD5 또는 oshash를 사용합니다. 이를 변경하려면 모든 영상에 해당 MD5/osash 값이 채워져 있어야 합니다. 이 값을 변경한 후에는 기존에 생성된 파일을 마이그레이션하거나 재생성해야 합니다. 마이그레이션은 '작업' 페이지를 참조하세요.", + "generated_file_naming_hash_head": "생성된 파일 이름 해쉬", "generated_files_location": "생성된 파일들의 폴더 위치 (영상 마커, 영상 미리보기, 스프라이트 등등)", "generated_path_head": "생성된 파일 경로", "hashing": "해싱", - "image_ext_head": "이미지 확장 프로그램", + "image_ext_desc": "이미지로 인식될 파일 확장자입니다 (쉼표로 구분합니다).", + "image_ext_head": "이미지 확장자", + "include_audio_desc": "미리보기를 생성할 때 소리를 포함합니다.", "include_audio_head": "소리 포함", "logging": "로깅", + "maximum_streaming_transcode_size_desc": "트랜스코딩된 스트림의 최대 크기", + "maximum_streaming_transcode_size_head": "최대 스트리밍 트랜스코드 크기", + "maximum_transcode_size_desc": "생성된 트랜스코드의 최대 크기", + "maximum_transcode_size_head": "최대 트랜스코드 크기", "metadata_path": { "description": "전체 내보내기 또는 전체 불러오기를 실행할 때 사용되는 폴더 위치", "heading": "메타데이터 경로" }, + "number_of_parallel_task_for_scan_generation_desc": "자동으로 설정하려면 0을 입력하세요. 경고: 100% CPU 활용률을 달성하는 데 필요한 작업보다 더 많은 작업을 실행하면 성능이 저하되고 잠재적으로 다른 문제가 발생할 수 있습니다.", + "number_of_parallel_task_for_scan_generation_head": "스캔/생성 병렬 작업 수", "parallel_scan_head": "병렬 스캔/생성", "preview_generation": "생성 미리보기", "python_path": { + "description": "파이썬 실행 파일의 위치입니다. 스크립트 스크레이퍼와 플러그인의 실행에 사용됩니다. 빈 칸으로 두면, 시스템 환경 설정으로부터 위치를 받아 옵니다", "heading": "Python 경로" }, + "scraper_user_agent": "스크레이퍼 사용자 에이전트", + "scraper_user_agent_desc": "HTTP 요청 스크레이핑 중 사용되는 사용자 에이전트 문자열", "scrapers_path": { "description": "스크레이퍼 설정 파일의 폴더 위치", "heading": "스크레이퍼 경로" }, "scraping": "스크레이핑", "sqlite_location": "SQLite 데이터베이스의 파일 위치 (설정 변경 후 재시작이 필요합니다)", - "video_ext_desc": "비디오로 인식되는 파일 확장자들의 목록입니다 (쉼표(,)로 구분).", + "video_ext_desc": "비디오로 인식될 파일 확장자입니다 (쉼표로 구분합니다).", "video_ext_head": "비디오 확장 프로그램", "video_head": "비디오" }, "library": { "exclusions": "제외", "gallery_and_image_options": "갤러리와 이미지 옵션", - "media_content_extensions": "미디어 컨텐츠 확장 프로그램" + "media_content_extensions": "미디어 컨텐츠 확장자" }, "logs": { "log_level": "로그 수준" }, + "plugins": { + "hooks": "후크", + "triggers_on": "트리거 켜기" + }, "scraping": { "entity_metadata": "{entityType} 메타데이터", "entity_scrapers": "{entityType} 스크레이퍼", + "excluded_tag_patterns_desc": "스크레이핑 결과에서 제외할 태그 이름의 정규표현식", + "excluded_tag_patterns_head": "제외된 태그 패턴", "scraper": "스크레이퍼", "scrapers": "스크레이퍼", "search_by_name": "이름으로 찾기", @@ -244,10 +325,21 @@ "stashbox": { "add_instance": "stash-box 인스턴스 추가", "api_key": "API 키", - "name": "이름" + "description": "Stash-box는 식별값 및 파일 이름을 기반으로 영상 및 배우를 자동 태깅합니다.\n엔드포인트 및 API 키는 Stash-Box 인스턴스의 계정 페이지에서 찾을 수 있습니다. 인스턴스를 두 개 이상 추가할 경우 이름이 필요합니다.", + "endpoint": "엔드포인트", + "graphql_endpoint": "GraphQL 엔드포인트", + "name": "이름", + "title": "Stash-box 엔드포인트" + }, + "system": { + "transcoding": "트랜스코딩" }, "tasks": { "added_job_to_queue": "작업 대기열에 {operation_name}을 추가했습니다", + "auto_tag": { + "auto_tagging_all_paths": "모든 경로 자동 태깅 중", + "auto_tagging_paths": "다음 경로 자동 태깅 중" + }, "auto_tag_based_on_filenames": "파일 이름을 통해 컨텐츠에 자동으로 태깅합니다.", "auto_tagging": "자동 태깅", "backing_up_database": "데이터베이스 백업 중", @@ -259,27 +351,54 @@ "dont_include_file_extension_as_part_of_the_title": "제목에 파일 확장자 포함하지 않기", "empty_queue": "실행 중인 작업이 없습니다.", "export_to_json": "메타데이터 폴더에 데이터베이스 컨텐츠를 JSON 형식으로 내보냅니다.", + "generate": { + "generating_from_paths": "다음 경로에서 영상 생성 중", + "generating_scenes": "{num}개의 {scene} 생성 중" + }, + "generate_desc": "이미지, 스프라이트, 비디오, vtt 등 파일을 생성합니다.", "generate_phashes_during_scan": "컨텐츠 해쉬 값 생성", "generate_phashes_during_scan_tooltip": "중복된 파일 확인과 영상 식별에 사용됩니다.", "generate_previews_during_scan": "움직이는 이미지 미리보기 생성", + "generate_previews_during_scan_tooltip": "애니메이션 WebP 미리보기를 생성합니다. 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", + "generate_sprites_during_scan": "스크러버 스프라이트 생성", "generate_thumbnails_during_scan": "이미지 썸네일 생성", "generate_video_previews_during_scan": "미리보기 생성", "generate_video_previews_during_scan_tooltip": "마우스를 위에 올려놓았을 때 재생되는 비디오 미리보기 생성", "generated_content": "생성된 컨텐츠", "identify": { + "and_create_missing": "또한 누락된 항목 생성", + "create_missing": "누락된 항목 생성", "default_options": "기본값 옵션", + "description": "Stash-Box 및 스크레이퍼 소스를 사용하여 영상 메타데이터를 자동으로 설정합니다.", + "explicit_set_description": "소스 별 옵션에서 재정의되지 않는 경우 다음 옵션이 사용됩니다.", + "field": "항목", + "field_behaviour": "{strategy} {field}", "field_options": "항목 옵션", + "heading": "식별", + "identifying_from_paths": "다음 경로에서 영상 식별 중", + "identifying_scenes": "{num}개의 {scene} 식별 중", "include_male_performers": "남성 배우 포함", - "set_cover_images": "커버 이미지 설정" + "set_cover_images": "커버 이미지 설정", + "set_organized": "'정리됨' 상태로 설정", + "source": "소스", + "source_options": "{source} 옵션", + "sources": "소스", + "strategy": "방법" }, + "import_from_exported_json": "메타데이터 폴더에서 내보낸 JSON 파일에서 가져오기 작업을 합니다. 기존 데이터베이스를 지웁니다.", + "incremental_import": "내보낸 zip 파일에서 증가한 부분만 가져옵니다.", "job_queue": "작업 대기열", "maintenance": "관리", + "migrate_hash_files": "생성된 파일 이름 해쉬를 변경한 후, 기존 생성된 파일의 이름을 새 해쉬 형식으로 바꾸기 위해 사용됩니다.", + "migrations": "마이그레이션", "only_dry_run": "체크만 합니다. 아무 것도 삭제하지 않습니다", + "plugin_tasks": "플러그인 작업", "scan": { - "scanning_all_paths": "모든 경로 스캔 중" + "scanning_all_paths": "모든 경로 스캔 중", + "scanning_paths": "다음 경로 스캔 중" }, "scan_for_content_desc": "새로운 컨텐츠를 스캔하고 데이터베이스에 추가합니다.", - "set_name_date_details_from_metadata_if_present": "파일 속성을 통해 이름, 날짜, 세부 사항들을 설정합니다" + "set_name_date_details_from_metadata_if_present": "파일 속성값으로 이름, 날짜, 세부 사항을 설정" }, "tools": { "scene_duplicate_checker": "영상 중복 체크 도구", @@ -287,9 +406,14 @@ "add_field": "항목 추가", "capitalize_title": "제목 앞 글자를 대문자로", "display_fields": "항목 표시하기", + "escape_chars": "\\를 사용하여 리터럴 문자를 이스케이프합니다", "filename": "파일 이름", "filename_pattern": "파일 이름 패턴", + "ignore_organized": "'정리됨' 상태의 영상을 무시", "ignored_words": "무시된 단어들", + "matches_with": "{i}와 일치", + "select_parser_recipe": "파서 레시피 선택", + "title": "영상 파일 이름 분석기", "whitespace_chars": "공백 문자", "whitespace_chars_desc": "이 문자들은 제목에서 공백으로 대체됩니다" }, @@ -304,9 +428,14 @@ }, "delete_options": { "description": "이미지, 갤러리, 영상을 삭제할 때의 설정 기본값입니다.", - "heading": "옵션 삭제" + "heading": "옵션 삭제", + "options": { + "delete_file": "기본값으로 파일을 지웁니다", + "delete_generated_supporting_files": "생성된 지원 파일을 기본값으로 삭제합니다" + } }, "desktop_integration": { + "desktop_integration": "데스크탑 통합", "notifications_enabled": "알림 활성화", "send_desktop_notifications_for_events": "이벤트가 발생했을 때 데스크탑 알림을 보냅니다", "skip_opening_browser": "브라우저 자동 열기 해제", @@ -320,6 +449,7 @@ "heading": "수정하기" }, "funscript_offset": { + "description": "대화형 스크립트 재생의 시간 오프셋(밀리초)입니다.", "heading": "Funscript 오프셋 (단위: 밀리초)" }, "handy_connection": { @@ -327,14 +457,35 @@ "server_offset": { "heading": "서버 오프셋" }, + "status": { + "heading": "Handy 연결 상태" + }, "sync": "동기화" }, - "images": { - "heading": "이미지" + "handy_connection_key": { + "description": "대화형 영상에 사용할 수 있는 Handy 연결 키입니다. 이 키를 설정하면 현재 장면 정보를 handyfeeling.com과 공유할 수 있습니다", + "heading": "Handy 연결 키" }, + "image_lightbox": { + "heading": "이미지 라이트박스" + }, + "images": { + "heading": "이미지", + "options": { + "write_image_thumbnails": { + "description": "즉시 생성된 경우 디스크에 이미지 썸네일 쓰기", + "heading": "이미지 썸네일 디스크에 저장하기" + } + } + }, + "interactive_options": "상호작용 옵션", "language": { "heading": "언어" }, + "max_loop_duration": { + "description": "영상 플레이어가 비디오를 루프하는 최대 영상 지속 시간 - 0을 입력하면 비활성화합니다", + "heading": "최대 구간 길이" + }, "menu_items": { "description": "탐색 바에 여러 종류의 컨텐츠들이 보여지게 하거나 숨깁니다", "heading": "메뉴 항목" @@ -342,6 +493,7 @@ "performers": { "options": { "image_location": { + "description": "배우의 기본 이미지를 저장하기 위한 경로입니다. 빈 칸으로 두면 기본값을 사용합니다", "heading": "커스텀 배우 이미지 경로" } } @@ -365,10 +517,15 @@ "heading": "영상 플레이어", "options": { "auto_start_video": "비디오 자동 재생", + "auto_start_video_on_play_selected": { + "description": "'영상' 페이지에서 선택하거나 랜덤 재생한 영상을 자동 시작", + "heading": "선택한 항목을 재생할 때 비디오 자동 시작" + }, "continue_playlist_default": { "description": "비디오가 끝나면 대기열에 있는 다음 영상을 재생합니다", "heading": "플레이리스트 이어보기" - } + }, + "show_scrubber": "스크러버 보이기" } }, "scene_wall": { @@ -378,13 +535,29 @@ "toggle_sound": "소리 켜기" } }, + "scroll_attempts_before_change": { + "description": "이전/다음 항목으로 이동하기 전에 스크롤을 시도하는 횟수입니다. Y축 스크롤 허용 모드에만 적용됩니다.", + "heading": "전환 전 스크롤 시도 횟수" + }, "slideshow_delay": { + "description": "월 보기 모드일 때 갤러리에서 슬라이드 쇼를 사용할 수 있습니다", "heading": "슬라이드쇼 딜레이 (단위: 초)" }, "title": "UI" } }, "configuration": "설정", + "countables": { + "files": "{count, plural, one {파일} other {파일들}}", + "galleries": "{count, plural, one {갤러리} other {갤러리들}}", + "images": "{count, plural, one {이미지} other {이미지들}}", + "markers": "{count, plural, one {마커} other {마커들}}", + "movies": "{count, plural, one {영화} other {영화들}}", + "performers": "{count, plural, one {배우} other {배우들}}", + "scenes": "{count, plural, one {영상} other {영상들}}", + "studios": "{count, plural, one {스튜디오} other {스튜디오들}}", + "tags": "{count, plural, one {태그} other {태그들}}" + }, "country": "국적", "cover_image": "커버 이미지", "created_at": "만든 날짜", @@ -395,17 +568,17 @@ }, "criterion_modifier": { "between": "구간", - "equals": "같음", + "equals": "=", "excludes": "포함하지 않음", "format_string": "{criterion} {modifierString} {valueString}", - "greater_than": "초과", + "greater_than": ">", "includes": "포함", "includes_all": "모두 포함", "is_null": "값 없음", - "less_than": "미만", + "less_than": "<", "matches_regex": "정규표현식 일치", "not_between": "구간 밖", - "not_equals": "같지 않음", + "not_equals": "≠", "not_matches_regex": "정규표현식 불일치", "not_null": "값 존재함" }, @@ -418,43 +591,101 @@ "details": "세부사항", "developmentVersion": "개발 버전", "dialogs": { + "aliases_must_be_unique": "별칭은 유일해야 합니다", + "delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:", "delete_confirm": "정말 {entityName}을 삭제하시겠습니까?", + "delete_entity_desc": "{정말로 {count, plural, one {singularEntity} other {pluralEntity}}을(를) 삭제하시겠습니까? 원본 파일 또한 삭제하지 않으면 스캔을 할 때 {count, plural, one {singularEntity} other {pluralEntity}}이(가) 다시 추가될 것입니다.}", + "delete_entity_title": "{count, plural, one {{singularEntity} 삭제} other {{pluralEntity} 삭제}}", + "delete_galleries_extra": "…그리고 다른 어떤 갤러리에도 없는 이미지 파일들까지.", + "delete_gallery_files": "갤러리 폴더/zip 파일 및 다른 어떤 갤러리에도 존재하지 않는 이미지를 삭제합니다.", + "delete_object_desc": "정말로 {count, plural, one {this {singularEntity}} other {these {pluralEntity}}}을(를) 삭제하시겠습니까?", + "delete_object_overflow": "...그리고 {count} 개의 {count, plural, one {{singularEntity}} other {{pluralEntity}}}.", + "delete_object_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 삭제", + "edit_entity_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 수정", + "export_include_related_objects": "내보내기 할 때 관련된 개체를 포합합니다", "export_title": "내보내기", "lightbox": { "delay": "딜레이 (단위: 초)", "display_mode": { "fit_horizontally": "가로로 맞추기", "fit_to_screen": "스크린 크기에 맞추기", - "label": "표시 모드" + "label": "표시 모드", + "original": "기본 모드" }, "options": "옵션", + "reset_zoom_on_nav": "이미지를 바꿀 때 줌 수준을 초기화합니다", + "scale_up": { + "description": "작은 이미지들이 화면을 채우도록 확대합니다", + "label": "화면에 딱 맞게 확대합니다" + }, "scroll_mode": { "description": "임시로 다른 모드를 사용하려면 Shift 키를 누르세요.", "label": "스크롤 모드", + "pan_y": "수직 스크롤 모드", "zoom": "확대" } }, + "merge_tags": { + "destination": "다른 태그와 합쳐질 태그", + "source": "다른 태그로 합쳐질 태그" + }, + "overwrite_filter_confirm": "정말 원래 저장되어 있었던 쿼리 {entityName}을 덮어쓰시겠습니까?", "scene_gen": { + "force_transcodes": "강제 트랜스코드 생성", + "force_transcodes_tooltip": "기본적으로 트랜스코드는 비디오 파일이 브라우저에서 지원되지 않는 경우에만 생성됩니다. 이 옵션을 선택하면 비디오 파일이 브라우저에서 지원되는 것으로 보이는 경우에도 트랜스코드가 생성됩니다.", "image_previews": "움직이는 이미지 미리보기", + "image_previews_tooltip": "애니메이션 WebP 미리보기, 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", + "interactive_heatmap_speed": "대화형 영상을 위한 히트맵 및 스피드 생성", "marker_image_previews": "마커 움직이는 이미지 미리보기", + "marker_image_previews_tooltip": "애니메이션 마커 WebP 미리보기, 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", "marker_screenshots": "마커 스크린샷", + "marker_screenshots_tooltip": "마커 JPG 이미지. 미리보기 유형이 이미지로 설정된 경우에만 필요합니다.", "markers": "마커 미리보기", + "markers_tooltip": "주어진 시간 코드에서 시작하는 20초 짜리 비디오입니다.", + "override_preview_generation_options": "미리보기 생성 옵션 재정의", + "override_preview_generation_options_desc": "이 작업에 대한 미리보기 생성 옵션을 재정의합니다. 기본값은 '시스템' -> '미리보기 생성'에서 설정됩니다.", "overwrite": "이미 생성된 파일들 덮어쓰기", "phash": "해쉬 (중복 방지용)", + "preview_exclude_end_time_desc": "영상 미리보기에서 마지막 x 초를 제외합니다. 초 단위, 혹은 전체 영상 길이에서의 비율(예: 2%)로 나타낼 수 있습니다.", + "preview_exclude_end_time_head": "마지막 영상 부분 제외", + "preview_exclude_start_time_desc": "영상 미리보기에서 처음 x 초를 제외합니다. 초 단위, 혹은 전체 영상 길이에서의 비율(예: 2%)로 나타낼 수 있습니다.", + "preview_exclude_start_time_head": "처음 영상 부분 제외", + "preview_generation_options": "미리보기 생성 옵션", "preview_options": "옵션 미리보기", - "preview_preset_head": "인코딩 프리셋 미리보기" + "preview_preset_desc": "이 설정은 영상 미리보기의 크기, 품질, 미리보기 생성 인코딩 시간을 조절합니다. 설정값을 높인다고 해서 결과가 비례하여 좋아지는 것이 아니므로, \"느림\" 이상으로 설정하는 것을 추천하지 않습니다.", + "preview_preset_head": "인코딩 프리셋 미리보기", + "preview_seg_count_desc": "미리보기 파일에서의 사진 개수입니다.", + "preview_seg_count_head": "미리보기의 사진 개수", + "preview_seg_duration_desc": "미리보기 사진이 표시되는 시간입니다 (초).", + "preview_seg_duration_head": "미리보기 사진 길이", + "sprites": "영상 스크러버 스프라이트", + "sprites_tooltip": "스프라이트 (영상 스크러버 용)", + "transcodes": "트랜스코딩", + "transcodes_tooltip": "지원되지 않는 비디오 형식을 MP4로 변환하기", + "video_previews": "미리보기", + "video_previews_tooltip": "영상 위로 마우스를 올렸을 때 표시되는 비디오 미리보기" }, + "scenes_found": "{count}개의 영상을 찾았습니다", + "scrape_entity_query": "{entity_type} 스크레이핑 쿼리", + "scrape_entity_title": "{entity_type} 스크레이핑 결과", + "scrape_results_existing": "존재", + "scrape_results_scraped": "스크레이핑됨", "set_image_url_title": "이미지 URL", "unsaved_changes": "저장되지 않은 변경 사항들이 있습니다. 그래도 나가겠습니까?" }, + "dimensions": "해상도", + "director": "감독", "display_mode": { "grid": "격자", "list": "목록", - "unknown": "알 수 없음" + "tagger": "태거", + "unknown": "알 수 없음", + "wall": "월 모드" }, "donate": "후원", "dupe_check": { "description": "'정확' 이하의 수준에서는 계산이 오래 걸릴 수 있습니다. 낮은 정밀도 수준에서는 부정확한 결과가 함께 나올 수 있습니다.", + "found_sets": "{setCount, plural, one{# 개의 중복된 파일을 찾았습니다.} other {# 개의 중복된 파일들을 찾았습니다.}}", "options": { "exact": "정확", "high": "높음", @@ -464,8 +695,10 @@ "search_accuracy_label": "검색 정밀도", "title": "중복된 영상" }, + "duplicated_phash": "중복됨 (perceptual hash)", "duration": "길이", "effect_filters": { + "aspect": "방향", "blue": "청색", "blur": "흐리게", "brightness": "밝기", @@ -485,8 +718,12 @@ "scale": "크기", "warmth": "따뜻함" }, + "empty_server": "이 페이지에서 추천 영상들을 확인하려면 영상을 추가하세요.", "ethnicity": "인종", + "existing_value": "존재하는 값", "eye_color": "눈동자 색", + "fake_tits": "가짜 가슴", + "false": "거짓", "favourite": "즐겨찾기", "file": "파일", "file_info": "파일 정보", @@ -498,10 +735,16 @@ "filters": "필터", "framerate": "프레임 레이트", "frames_per_second": "초당 프레임: {value}", + "front_page": { + "types": { + "premade_filter": "생성된 필터", + "saved_filter": "저장된 필터" + } + }, "galleries": "갤러리", "gallery": "갤러리", "gallery_count": "갤러리 개수", - "gender": "성", + "gender": "성별", "gender_types": { "FEMALE": "여성", "INTERSEX": "인터섹스", @@ -514,10 +757,13 @@ "handy_connection_status": { "connecting": "접속 중", "disconnected": "접속 끊김", + "error": "Handy에 접속 중 오류 발생", + "missing": "연결 끊김", "ready": "준비됨", "syncing": "서버와 동기화 중", "uploading": "스크립트 업로드 중" }, + "hasMarkers": "마커 유무", "height": "키", "help": "도움말", "ignore_auto_tag": "자동 태깅 무시하기", @@ -528,39 +774,54 @@ "include_sub_studios": "자회사 스튜디오 포함", "include_sub_tags": "하위 태그 포함", "instagram": "인스타그램", + "interactive": "인터렉티브", + "interactive_speed": "인터랙티브 속도", + "isMissing": "데이터 누락됨", "library": "라이브러리", "loading": { "generic": "로드 중…" }, "marker_count": "마커 개수", "markers": "마커", + "measurements": "치수", "media_info": { "audio_codec": "오디오 코덱", "checksum": "체크섬", "downloaded_from": "다운로드 출처", "hash": "해쉬", + "interactive_speed": "인터랙티브 속도", "performer_card": { + "age": "{age} {years_old}", "age_context": "작품에서 {age} {years_old}" }, + "phash": "PHash", "stream": "스트림", "video_codec": "비디오 코덱" }, "megabits_per_second": "초당 {value} 메가비트", "metadata": "메타데이터", "movie": "영화", + "movie_scene_number": "영화 씬 번호", "movies": "영화", "name": "이름", + "new": "새로 만들기", + "none": "없음", "o_counter": "싼 횟수", + "operations": "작업", + "organized": "정리됨", "pagination": { "first": "처음", "last": "마지막", "next": "다음", "previous": "이전" }, + "parent_of": "{children}의 상위 태그", "parent_studios": "모회사 스튜디오", "parent_tag_count": "상위 태그 개수", "parent_tags": "상위 태그", + "part_of": "{parent}의 하위 태그", "path": "경로", + "perceptual_similarity": "영상 정렬에서 선택할 수 있습니다.", "performer": "배우", "performerTags": "배우 태그", "performer_age": "배우 나이", @@ -569,6 +830,9 @@ "performer_image": "배우 이미지", "performer_tagger": { "add_new_performers": "새 배우 추가", + "any_names_entered_will_be_queried": "입력되는 이름들은, 원격 Stash-Box 개체에 존재하면 추가됩니다. 정확하게 일치해야만 합니다.", + "batch_add_performers": "배우 일괄 추가", + "batch_update_performers": "배우 일괄 수정", "config": { "active_stash-box_instance": "stash-box 인스턴스 활성화:", "edit_excluded_fields": "제외된 항목 수정", @@ -582,30 +846,33 @@ "name_already_exists": "이름이 이미 존재합니다", "network_error": "네트워크 오류", "no_results_found": "결과가 없습니다.", + "number_of_performers_will_be_processed": "{performer_count}명의 배우들이 처리됩니다", + "performer_already_tagged": "이 배우에 이미 존재하는 태그입니다", "performer_names_separated_by_comma": "배우 이름 (,으로 구분)", + "performer_selection": "배우 선택", "performer_successfully_tagged": "배우 태깅에 성공했습니다:", "query_all_performers_in_the_database": "데이터베이스의 모든 배우", "refresh_tagged_performers": "태그된 배우 새로고침", + "refreshing_will_update_the_data": "새로고침하면 Stash-box 인스턴스에 있는 태그된 배우들의 데이터가 업데이트될 것입니다.", "status_tagging_job_queued": "상태: 태그 작업 대기열 추가됨", "status_tagging_performers": "상태: 배우 태그 중", "tag_status": "태그 상태", "to_use_the_performer_tagger": "배우 태거를 사용하기 위해서는 stash-box 인스턴스가 설정되어야 합니다.", "untagged_performers": "태그되지 않은 배우", "update_performer": "배우 수정", - "update_performers": "배우 수정" + "update_performers": "배우 수정", + "updating_untagged_performers_description": "태그가 지정되지 않은 배우를 업데이트하면, Stash ID가 없는 배우와 비교해본 뒤 메타데이터를 업데이트할 것입니다." }, "performers": "배우", "piercings": "피어싱", "queue": "대기열", "random": "랜덤", "rating": "별점", - "recently_added_performers": "최근에 추가된 배우", - "recently_added_studios": "최근에 추가된 스튜디오", - "recently_released_galleries": "최근에 만들어진 갤러리", - "recently_released_movies": "최근에 만들어진 영화", - "recently_released_scenes": "최근에 만들어진 영상", + "recently_added_objects": "최근 추가된 {objects}", + "recently_released_objects": "최근 발매된 {objects}", "resolution": "해상도", "scene": "영상", + "sceneTagger": "영상 태거", "sceneTags": "영상 태그", "scene_count": "영상 개수", "scene_id": "영상 ID", @@ -646,10 +913,15 @@ "github_repository": "깃허브 저장소", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "백업 데이터베이스 경로 (백업을 하지 않으려면 빈 칸으로 두세요):", + "backup_recommended": "마이그레이션 하기 전 원래 데이터베이스를 백업하는 것을 추천합니다. {defaultBackupPath}에 데이터베이스 복사본을 만들어 드릴 수 있습니다.", "migrating_database": "데이터베이스 마이그레이션 중", "migration_failed": "마이그레이션 실패", "migration_failed_error": "데이터베이스를 마이그레이션 하는 동안 다음 에러가 발생했습니다:", - "perform_schema_migration": "스키마 마이그레이션 실행" + "migration_failed_help": "올바른 내용을 입력했는지 확인하고 수정한 뒤 다시 시도해보세요. 그렇지 않다면, {githubLink}에 버그를 제보하거나 {discordLink}에서 도움이 될 만한 정보를 찾아보세요.", + "migration_irreversible_warning": "스키마 마이그레이션 작업은 돌이킬 수 없습니다. 마이그레이션이 진행된 이후에는, 데이터베이스가 이전 버전의 Stash와 호환되지 않을 것입니다.", + "migration_required": "마이그레이션 필요", + "perform_schema_migration": "스키마 마이그레이션 실행", + "schema_too_old": "현재 Stash 데이터베이스의 스키마 버전은 {databaseSchema}이고, {appSchema} 버전으로 마이그레이션되어야 합니다.이 Stash 버전은 데이터베이스 마이그레이션 없이는 동작하지 않을 것입니다." }, "paths": { "database_filename_empty_for_default": "데이터베이스 파일 이름 (빈 칸으로 두면 기본값을 사용합니다)", @@ -660,30 +932,48 @@ "where_can_stash_store_its_database": "어디에 Stash 데이터베이스를 저장할까요?", "where_can_stash_store_its_database_description": "Stash는 야동 메타데이터를 저장할 때 sqlite 데이터베이스를 사용합니다. 기본값으로, 데이터베이스 파일은 stash-go.sqlite라는 이름으로 설정 파일이 포함된 폴더 안에 생성될 것입니다. 데이터베이스 파일 이름을 바꾸고 싶다면, 절대 경로 또는 상대 경로(현재 경로 기준)를 입력하세요.", "where_can_stash_store_its_generated_content": "생성된 컨텐츠를 어디에 저장할까요?", + "where_can_stash_store_its_generated_content_description": "Stash에서는 썸네일, 미리보기, 스프라이트로 사용할 이미지와 비디오 파일을 생성합니다. 여기에는 지원되지 않는 파일 형식들의 변환본도 포함됩니다. Stash에서는 기본값으로, 설정 파일이 위치한 폴더 안에 generated 폴더를 만들 것입니다. 생성된 미디어 파일들이 저장되는 위치를 변경하고 싶다면, 절대 경로 혹은 상대 경로(현재 폴더 기준)를 적어주세요. 적혀진 경로에 해당 폴더가 없다면 자동으로 생성됩니다.", "where_is_your_porn_located": "야동이 있는 위치가 어딘가요?", "where_is_your_porn_located_description": "야동 폴더를 추가하세요. 비디오와 이미지를 스캐닝할 때 사용됩니다." }, "stash_setup_wizard": "Stash 설정 마법사", "success": { + "getting_help": "도움 받기", "help_links": "문제가 발생하거나 질문, 제안할 점이 있다면, {githubLink}에 이슈를 만들거나, {discordLink}의 커뮤니티를 방문하세요.", "in_app_manual_explained": "상단 우측에 있는 아이콘({icon})을 통해 매뉴얼을 확인해보세요", + "next_config_step_one": "다음으로 설정 페이지에 갈 것입니다. 설정 페이지에서는 포함하거나 제외시킬 파일 설정, 시스템을 보호할 아이디와 비밀번호 설정, 그리고 그 외 여러 가지 옵션들을 설정합니다.", + "next_config_step_two": "이 설정에 만족한다면, {localized_task} 버튼과 {localized_scan} 버튼을 눌러 여러분의 컨텐츠를 스캔할 수 있습니다.", + "open_collective": "Stash가 지속적으로 업데이트되도록 하기 위해 어떻게 기여할 수 있는지 보려면 {open_collective_link}를 확인해보세요.", "support_us": "후원", "thanks_for_trying_stash": "Stash를 사용해주셔서 감사합니다!", + "welcome_contrib": "프로그래밍, 테스팅, 버그 제보, 개선 또는 기능 추가 요청, 사용자 지원 등에 기여하는 것을 환영합니다. Stash 인앱 매뉴얼의 '기여' 항목에서 세부사항을 확인할 수 있습니다.", "your_system_has_been_created": "성공했습니다! 시스템이 생성되었습니다!" }, "welcome": { + "config_path_logic_explained": "Stash에서는 설정 파일을 현재 폴더에서 먼저 찾아보고, 없다면 %USERPROFILE%\\.stash\\config.yml을 찾습니다 (윈도우가 아닌 운영체제에서는 $HOME/.stash/config.yml을 찾습니다). 또는 Stash를 -c <설정 파일 경로> 또는 --config <설정 파일 경로> 옵션을 사용해 실행시켜 특정한 설정 파일을 읽도록 할 수 있습니다.", + "in_current_stash_directory": "$HOME/.stash 폴더 안", + "in_the_current_working_directory": "현재 폴더 안", + "next_step": "그 모든 것을 제외하고, 새로운 시스템 설정을 시작할 준비가 되었다면, 어디에 설정 파일을 저장할지 선택한 뒤 '다음' 버튼을 누르세요.", "store_stash_config": "어디에 Stash 설정 파일을 저장할까요?", "unable_to_locate_config": "이 화면이 나온다면, 설정 파일을 찾는 데에 실패한 것입니다. 새로운 설정 파일을 만드는 과정을 거쳐야 합니다.", "unexpected_explained": "예상치 못하게 이 화면이 나온다면, 올바른 폴더에서 Stash를 재실행하거나, 터미널의 경우 -c 플래그와 함께 실행해보세요." }, "welcome_specific_config": { "config_path": "Stash에서 다음 설정 파일 경로를 사용합니다: {path}", - "next_step": "새로운 시스템을 설정할 준비가 되었다면, '다음'을 누르세요." + "next_step": "새로운 시스템을 설정할 준비가 되었다면, '다음'을 누르세요.", + "unable_to_locate_specified_config": "이 오류 문구가 출력되었다면, 명령어 또는 환경에서 지정된 설정 파일을 찾지 못한 것입니다. 설정 마법사가 새로운 설정 파일을 만드는 과정을 도와줄 것입니다." }, "welcome_to_stash": "Stash에 오신 것을 환영합니다" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", + "stashbox": { + "go_review_draft": "초안을 검토하려면 {endpoint_name}(으)로 이동하십시오.", + "selected_stash_box": "Stash-Box 엔드포인트를 선택했습니다", + "submission_failed": "데이터 제출 실패", + "submission_successful": "데이터 제출 성공", + "submit_update": "이미 {endpoint_name}에 있음" + }, "statistics": "통계", "stats": { "image_size": "전체 이미지 크기", @@ -709,7 +999,11 @@ "added_generation_job_to_queue": "생성 작업을 대기열에 추가했습니다", "created_entity": "{entity}를 생성했습니다", "default_filter_set": "기본 필터 셋", + "delete_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 삭제", + "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 삭제되었습니다", "generating_screenshot": "스크린샷을 생성하는 중…", + "merged_tags": "병합된 태그", + "rescanning_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 다시 스캔하는 중…", "saved_entity": "{entity}를 저장했습니다", "started_auto_tagging": "자동 태깅을 시작했습니다", "started_generating": "생성을 시작했습니다", @@ -719,10 +1013,11 @@ "total": "전체", "true": "참", "twitter": "트위터", + "type": "유형", "updated_at": "수정 날짜", "url": "URL", "videos": "비디오", "view_all": "모두 보기", - "weight": "무게", + "weight": "몸무게", "years_old": "살" } diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 8c2a4fe70..1de48a0b1 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -23,6 +23,7 @@ "create_entity": "Utwórz {entityType}", "create_marker": "Utwórz znacznik", "created_entity": "Utworzono {entity_type}: {entity_name}", + "customise": "Dostosuj", "delete": "Usuń", "delete_entity": "Usuń {entityType}", "delete_file": "Usuń plik", @@ -100,6 +101,7 @@ "stop": "Zatrzymaj", "submit": "Wyślij", "submit_stash_box": "Wyślij do Stash-Box", + "submit_update": "Prześlij aktualizację", "tasks": { "clean_confirm_message": "Czy na pewno chcesz przeprowadzić oczyszczanie? Spowoduje to usunięcie informacji o bazie danych i wygenerowanej zawartości dla wszystkich scen i galerii, które nie znajdują się już w systemie plików.", "dry_mode_selected": "Wybrano tryb próby na sucho. Nie nastąpi faktyczne usunięcie, a jedynie zapisanie w dzienniku.", @@ -733,6 +735,12 @@ "filters": "Filtry", "framerate": "Liczba klatek na sekundę", "frames_per_second": "{value} klatek na sekundę", + "front_page": { + "types": { + "premade_filter": "Gotowy filtr", + "saved_filter": "Zapisany filtr" + } + }, "galleries": "Galerie", "gallery": "Galeria", "gallery_count": "Liczba Galerii", @@ -860,11 +868,8 @@ "queue": "Kolejka", "random": "Losowo", "rating": "Ocena", - "recently_added_performers": "Niedawno Dodani Wykonawcy", - "recently_added_studios": "Niedawno Dodane Studia", - "recently_released_galleries": "Niedawno Dodane Galerie", - "recently_released_movies": "Niedawno Dodane Filmy", - "recently_released_scenes": "Niedawno Dodane Sceny", + "recently_added_objects": "Ostatnio dodane {objects}", + "recently_released_objects": "Ostatnio wydane {objects}", "resolution": "Rozdzielczość", "scene": "Scena", "sceneTagger": "Otagowywacz scen", @@ -966,7 +971,8 @@ "go_review_draft": "Przejdź do {endpoint_name}, aby zapoznać się z projektem.", "selected_stash_box": "Wybrany punkt końcowy Stash-Box", "submission_failed": "Zgłoszenie nie powiodło się", - "submission_successful": "Zgłoszenie przesłano sukcesem" + "submission_successful": "Zgłoszenie przesłano sukcesem", + "submit_update": "Już istnieje w {endpoint_name}" }, "statistics": "Statystyki", "stats": { @@ -1007,6 +1013,7 @@ "total": "Łącznie", "true": "Tak", "twitter": "Twitter", + "type": "Typ", "updated_at": "Zaktualizowano", "url": "URL", "videos": "Filmy wideo", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index 6ce5551f7..e9e79828f 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -7,9 +7,9 @@ "allow": "Permitir", "allow_temporarily": "Permitir temporariamente", "apply": "Aplicar", - "auto_tag": "Auto tag", + "auto_tag": "Etiquetamento automático", "backup": "Backup", - "browse_for_image": "Navegar imagens…", + "browse_for_image": "Procurar por imagem…", "cancel": "Cancelar", "clean": "Limpar", "clear": "Limpar", @@ -23,15 +23,18 @@ "create_entity": "Criar {entityType}", "create_marker": "Criar marcador", "created_entity": "Criar {entity_type}: {entity_name}", + "customise": "Customizar", "delete": "Apagar", "delete_entity": "Apagar {entityType}", "delete_file": "Apagar arquivo", "delete_file_and_funscript": "Deletar arquivo (e funscript)", "delete_generated_supporting_files": "Apagar arquivos gerados de suporte", + "delete_stashid": "Deletar StashID", "disallow": "Não permitir", "download": "Download", "download_backup": "Download backup", "edit": "Editar", + "edit_entity": "Editar {entityType}", "export": "Exportar…", "export_all": "Exportar tudo…", "find": "Encontrar", @@ -49,12 +52,14 @@ "identify": "Identificar", "ignore": "Ignorar", "import": "Importar…", - "import_from_file": "Importar do arquivo", + "import_from_file": "Importar de arquivo", + "logout": "Sair", "merge": "Unir", "merge_from": "Unir do", "merge_into": "Unir em", "next_action": "Próximo", "not_running": "não realizado", + "open_in_external_player": "Abrir em um reprodutor externo", "open_random": "Abrir aleatório", "overwrite": "Sobrescrever", "play_random": "Tocar aleatório", @@ -65,6 +70,7 @@ "reload_plugins": "Recarregar plugins", "reload_scrapers": "Recarregar scrapers", "remove": "Remover", + "remove_from_gallery": "Remover da galeria", "rename_gen_files": "Renomear arquivos gerados", "rescan": "Reescanear", "reshuffle": "Reembaralhar", @@ -79,9 +85,10 @@ "scrape_with": "Scrape com…", "search": "Buscar", "select_all": "Selecionar todos", + "select_entity": "Selecionar {entityType}", "select_folders": "Selecionar pastas", "select_none": "Selecionar nenhum", - "selective_auto_tag": "Auto Tag seletivo", + "selective_auto_tag": "Etiquetamento automático seletivo", "selective_clean": "Limpeza Seletiva", "selective_scan": "Escaneamento seletivo", "set_as_default": "Aplicar como padrão", @@ -94,13 +101,15 @@ "stop": "Parar", "submit": "Enviar", "submit_stash_box": "Enviar para o Stash-Box", + "submit_update": "Enviar atualização", "tasks": { "clean_confirm_message": "Tem certeza de que quer limpar? Isto irá apagar as informações do banco de dados e conteúdos gerados de todas as cenas e galerias que não são mais encontradas no sistema.", - "dry_mode_selected": "Modo não destrutivo. Nenhum arquivo será apagado, apenas será logado.", + "dry_mode_selected": "Modo não destrutivo. Nenhum arquivo será apagado, apenas registrado.", "import_warning": "Tem certeza de que quer importar? Isto irá apagar o banco de dados e re-importar de seus metadados exportados." }, "temp_disable": "Desabilitar temporariamente…", "temp_enable": "Habilitar temporariamente…", + "unset": "Desaplicar", "use_default": "Usar padrão", "view_random": "Mostrar aleatoriamente" }, @@ -114,10 +123,11 @@ "birth_year": "Ano de nascimento", "birthdate": "Data de nascimento", "bitrate": "Taxa de bits", + "captions": "Legendas", "career_length": "Duração da carreira", "component_tagger": { "config": { - "active_instance": "Ativar stash-box:", + "active_instance": "Instância do stash-box ativa:", "blacklist_desc": "Os itens da lista negra são excluídos das consultas. Observe que são expressões regulares e também não fazem distinção entre maiúsculas e minúsculas. Certos caracteres devem ser escritos com uma barra invertida: {chars_require_escape}", "blacklist_label": "Lista negra", "query_mode_auto": "Automático", @@ -133,23 +143,23 @@ "query_mode_path_desc": "Usa o caminho inteiro do arquivo", "set_cover_desc": "Substitua a capa da cena se alguma for encontrada.", "set_cover_label": "Definir imagem da capa da cena", - "set_tag_desc": "Anexar tags à cena, sobrescrevendo ou mesclando com as tags existentes na cena.", - "set_tag_label": "Definir tags", - "show_male_desc": "Artistas masculinos estarão disponíveis para tag.", + "set_tag_desc": "Anexar etiquetas à cena, sobrescrevendo ou mesclando com as etiquetas existentes na cena.", + "set_tag_label": "Definir etiquetas", + "show_male_desc": "Define se artistas masculinos estarão disponíveis para etiquetar.", "show_male_label": "Mostrar artistas masculinos", "source": "Fonte" }, "noun_query": "Query", "results": { - "duration_off": "Duração por pelo menos {number}s", + "duration_off": "Duração fora por pelo menos {number}s", "duration_unknown": "Duração desconhecida", "fp_found": "{fpCount, plural, =0 {Nenhuma nova correspondência de impressão digital encontrada} other{# novas correspondências de impressão digital encontradas}}", "fp_matches": "Duração é uma correspondência", "fp_matches_multi": "Duração corresponde {matchCount}/{durationsLength} impressão digital(s)", "hash_matches": "{hash_type} é uma correspondência", - "match_failed_already_tagged": "Cena já marcada", + "match_failed_already_tagged": "Cena já etiquetada", "match_failed_no_result": "Nenhum resultado encontrado", - "match_success": "Cena marcada com sucesso", + "match_success": "Cena etiquetada com sucesso", "phash_matches": "{count} PHashes coincide(m)", "unnamed": "Sem nome" }, @@ -162,16 +172,16 @@ }, "config": { "about": { - "build_hash": "Build hash:", - "build_time": "Tempo de build:", + "build_hash": "Hash do executável:", + "build_time": "Horário de criação do executável:", "check_for_new_version": "Verificar se há uma nova versão", "latest_version": "Última versão", - "latest_version_build_hash": "Build Hash da última versão:", + "latest_version_build_hash": "Hash do executável da última versão:", "new_version_notice": "[NOVA]", - "stash_discord": "Junte-se ao nosso {url} canal", + "stash_discord": "Junte-se ao nosso servidor no {url}", "stash_home": "Stash home no {url}", "stash_open_collective": "Apoie-nos através de {url}", - "stash_wiki": "Stash {url} página", + "stash_wiki": "Página da Stash {url}", "version": "Versão" }, "application_paths": { @@ -180,10 +190,10 @@ "categories": { "about": "Sobre", "interface": "Interface", - "logs": "Logs", + "logs": "Registros", "metadata_providers": "Provedores de Meta-dados", "plugins": "Plugins", - "scraping": "Scraping", + "scraping": "Extração", "security": "Segurança", "services": "Serviços", "system": "Sistema", @@ -193,14 +203,19 @@ "dlna": { "allow_temp_ip": "Permitir {tempIP}", "allowed_ip_addresses": "Endereços de IP permitidos", - "default_ip_whitelist": "Whitelist de IP padrão", + "allowed_ip_temporarily": "IP permitido temporariamente", + "default_ip_whitelist": "Lista branca de IP padrão", "default_ip_whitelist_desc": "Endereços IP padrão permitidos a acessar DLNA. Use {wildcard} para permitir todos endereços de IP.", + "disabled_dlna_temporarily": "DLNA desativado temporariamente", + "disallowed_ip": "IP não permitido", "enabled_by_default": "Ativado por padrão", + "enabled_dlna_temporarily": "DLNA ativado temporariamente", "network_interfaces": "Interfaces", "network_interfaces_desc": "Interfaces para expor servidor DLNA ativo. Uma lista vazia resulta em execução em todas as interfaces. Requer DLNA ser reiniciado depois de alterar.", "recent_ip_addresses": "Endereços de IP recentes", "server_display_name": "Nome de exibição do servidor", "server_display_name_desc": "Nome de exibição do servidor DLNA. Padrão de {server_name} se vazio.", + "successfully_cancelled_temporary_behaviour": "Comportamento temporário cancelado com sucesso", "until_restart": "até reiniciar" }, "general": { @@ -214,12 +229,12 @@ "heading": "Credenciais" }, "generate_api_key": "Gerar Chave de API", - "log_file": "Arquivo de log", - "log_file_desc": "Caminho para o arquivo para o log de saída. Em branco para desativar o registro de arquivos. Requer reinicialização.", - "log_http": "Log de acesso http", - "log_http_desc": "Logs de acesso http para o terminal. Requer reinicialização.", - "log_to_terminal": "Log para terminal", - "log_to_terminal_desc": "Logs para o terminal, além de um arquivo. Sempre ativo se o log de arquivos estiver desativado. Requer reinicialização.", + "log_file": "Arquivo de registro", + "log_file_desc": "Caminho para o arquivo para mandar os registros. Deixe em branco para desativar o arquivo de registro. Requer reinicialização.", + "log_http": "Registrar acesso http", + "log_http_desc": "Registrar acesso http para o terminal. Requer reinicialização.", + "log_to_terminal": "Registrar para o terminal", + "log_to_terminal_desc": "Registrar para o terminal além do arquivo. Sempre ativo se o arquivo de registro estiver desativado. Requer reinicialização.", "maximum_session_age": "Tempo máximo da sessão", "maximum_session_age_desc": "Tempo ocioso máximo antes de uma sessão de login expirar, em segundos.", "password": "Senha", @@ -230,7 +245,7 @@ }, "cache_location": "Localização do diretório do cache", "cache_path_head": "Caminho do cache", - "calculate_md5_and_ohash_desc": "Calcular MD5 checksum além do oshash. A ativação fará com que as varreduras iniciais sejam mais lentas. Nomeação de arquivo Hash deve ser definido para oshash para desabilitar o cálculo MD5.", + "calculate_md5_and_ohash_desc": "Calcular MD5 checksum além do oshash. A ativação fará com que as escaneamentos iniciais sejam mais lentos. Nomeação de arquivo Hash deve ser definido para oshash para desabilitar o cálculo MD5.", "calculate_md5_and_ohash_label": "Calcular MD5 para vídeos", "check_for_insecure_certificates": "Verifique se há certificados inseguros", "check_for_insecure_certificates_desc": "Alguns sites usam ssl certificados inseguros. Quando desmarcado o scraper pula a verificação de certificados inseguros e permite o scraping desses sites. Se você receber um erro de certificado quando scraping desmarque isto.", @@ -240,9 +255,9 @@ "create_galleries_from_folders_label": "Crie galerias de pastas contendo imagens", "db_path_head": "Caminho do banco de dados", "directory_locations_to_your_content": "Locais de diretório para o seu conteúdo", - "excluded_image_gallery_patterns_desc": "Regexps de imagem e galeria de arquivos/caminhos para excluir da Varredura e adicionar para Limpar", + "excluded_image_gallery_patterns_desc": "Regexps de imagem e galeria de arquivos/caminhos para excluir do escaneamento e adicionar para limpar", "excluded_image_gallery_patterns_head": "Padrões de imagem/galeria excluidos", - "excluded_video_patterns_desc": "Regexps de video arquivos/caminhos para excluir da Varredura e adicionar para Limpar", + "excluded_video_patterns_desc": "Regexps de video arquivos/caminhos para excluir do escaneamento e adicionar para limpar", "excluded_video_patterns_head": "Padrões de vídeo excluidos", "gallery_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como arquivos ZIP da galeria.", "gallery_ext_head": "Extensões zip da galeria", @@ -250,31 +265,35 @@ "generated_file_naming_hash_head": "Hash de nomeação de arquivo gerado", "generated_files_location": "Local de diretório para os arquivos gerados (marcadores de cena, pré visualizações de cena, sprites, etc)", "generated_path_head": "Caminho gerado", - "hashing": "Hashing", + "hashing": "Criação de Hash", "image_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como imagens.", "image_ext_head": "Extensões de imagem", "include_audio_desc": "Inclui stream de áudio quando gerar pré-visualizações.", "include_audio_head": "Incluir áudio", - "logging": "Logging", - "maximum_streaming_transcode_size_desc": "Tamanho máximo para streams transcodados", + "logging": "Registro", + "maximum_streaming_transcode_size_desc": "Tamanho máximo para streams transcodificadas", "maximum_streaming_transcode_size_head": "Tamanho máximo de transcodação de streaming", - "maximum_transcode_size_desc": "Tamanho máximo para transcodes gerados", + "maximum_transcode_size_desc": "Tamanho máximo para transcodificações geradas", "maximum_transcode_size_head": "Tamanho máximo de transcodificação", "metadata_path": { "description": "Localização do diretório usado durante importação ou exportação completa dos meta-dados", - "heading": "Caminho dos Meta-dados" + "heading": "Caminho dos Metadados" }, "number_of_parallel_task_for_scan_generation_desc": "Defina como 0 para detecção automática. AVISO Execução de mais tarefas do que é necessário para obter 100% de utilização da CPU diminuirá o desempenho e potencialmente causar outros problemas.", - "number_of_parallel_task_for_scan_generation_head": "Número de tarefas paralelas para varredura/geração", - "parallel_scan_head": "Varredura/Geração paralela", - "preview_generation": "Pré visualizar a geração", - "scraper_user_agent": "Scraper User Agent", + "number_of_parallel_task_for_scan_generation_head": "Número de tarefas paralelas para escaneamento/geração", + "parallel_scan_head": "Escaneamento/geração paralela", + "preview_generation": "Geração de pré-visualização", + "python_path": { + "description": "Caminho do executável do Python. Utilizado para scripts de scrape e plugins. Se em branco, o caminho do Python será resolvido a partir do ambiente", + "heading": "Caminho do Python" + }, + "scraper_user_agent": "Agente de Usuário do Extrator", "scraper_user_agent_desc": "User-Agent string usado durante solicitações http do scrape", "scrapers_path": { "description": "Caminho para o diretório para os arquivos de configuração de scrapers", "heading": "Caminho dos scrapers" }, - "scraping": "Scraping", + "scraping": "Extração", "sqlite_location": "Localização do arquivo para o banco de dados SQLite (requer reinicialização)", "video_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como vídeos.", "video_ext_head": "Extensões de vídeo", @@ -286,19 +305,19 @@ "media_content_extensions": "Extensões de arquivo de mídia" }, "logs": { - "log_level": "Log Level" + "log_level": "Nível de registro" }, "plugins": { - "hooks": "Hooks", + "hooks": "Ganchos", "triggers_on": "Triggers on" }, "scraping": { "entity_metadata": "{entityType} metadados", "entity_scrapers": "Scrapers de {entityType}", - "excluded_tag_patterns_desc": "Expressões regulares de tags para excluir dos resultados da busca", - "excluded_tag_patterns_head": "Padrões de Tag Excluídos", - "scraper": "Scraper", - "scrapers": "Scrapers", + "excluded_tag_patterns_desc": "Expressões regulares de etiquetas para excluir dos resultados da busca", + "excluded_tag_patterns_head": "Padrões de etiqueta excluídos", + "scraper": "Extrator", + "scrapers": "Extratores", "search_by_name": "Buscar por nome", "supported_types": "Tipos suportados", "supported_urls": "URLs" @@ -306,8 +325,8 @@ "stashbox": { "add_instance": "Adicionar uma instância stash-box", "api_key": "Chave de API", - "description": "Stash-box facilita o tagging automático de cenas e artistas baseados em 'impressões digitais' e nomes de arquivos..\nEndpoint e chave de API pode ser encontrado na sua página de conta na instancia stash-box. Os nomes são necessários quando mais de uma instância são adicionados.", - "endpoint": "Endpoint", + "description": "Stash-box facilita o etiquetamento automático de cenas e artistas baseados em 'impressões digitais' e nomes de arquivos.\nEndpoint e chave de API pode ser encontrado na sua página de conta na instancia stash-box. Os nomes são necessários quando mais de uma instância são adicionados.", + "endpoint": "Ponto Final", "graphql_endpoint": "Endpoint GraphQL", "name": "Nome", "title": "Endpoints do Stash-box" @@ -318,11 +337,11 @@ "tasks": { "added_job_to_queue": "{operation_name} adicionada para a fila de trabalho", "auto_tag": { - "auto_tagging_all_paths": "Adicionar tags automaticamente em todos os caminhos", - "auto_tagging_paths": "Adicionar tags automaticamente nos seguintes caminhos" + "auto_tagging_all_paths": "Etiquetar automaticamente todos os caminhos", + "auto_tagging_paths": "Etiquetar automaticamente os seguintes caminhos" }, - "auto_tag_based_on_filenames": "Conteúdo automático de tag baseado em nomes de arquivos.", - "auto_tagging": "Auto tagging", + "auto_tag_based_on_filenames": "Etiquetar automaticamente conteúdo baseado em nomes de arquivos.", + "auto_tagging": "Etiquetamento automático", "backing_up_database": "Backup do banco de dados", "backup_and_download": "Executa um backup do banco de dados e baixa do arquivo resultante.", "backup_database": "Executa um backup do banco de dados para o mesmo diretório que o banco de dados, com o formato do nome do arquivo {filename_format}", @@ -372,13 +391,13 @@ "maintenance": "Manutenção", "migrate_hash_files": "Usado depois de alterar o hash gerado de nomeação de arquivos para renomear arquivos gerados existentes para o novo formato hash.", "migrations": "Migrações", - "only_dry_run": "Executar apenas modo não destrutivo. Não remova nada", + "only_dry_run": "Executar em modo não destrutivo. Não remova nada", "plugin_tasks": "Tarefas de plugin", "scan": { "scanning_all_paths": "Escaneando todos os caminhos", "scanning_paths": "Escaneando os seguintes caminhos" }, - "scan_for_content_desc": "Varre para novos conteúdos e adicioná-los ao banco de dados.", + "scan_for_content_desc": "Escaneie por novos conteúdos e os adicione ao banco de dados.", "set_name_date_details_from_metadata_if_present": "Definir nome, data e detalhes a partir dos meta-dados do arquivo" }, "tools": { @@ -433,10 +452,23 @@ "description": "Compensação de tempo em milissegundos para a reprodução de scripts interativos.", "heading": "Compensação de tempo Funscript (ms)" }, + "handy_connection": { + "connect": "Conectar", + "server_offset": { + "heading": "Compensação do Servidor" + }, + "status": { + "heading": "Estado da conexão do Handy" + }, + "sync": "Sincronizar" + }, "handy_connection_key": { "description": "Chave de conexão para usar em cenas interativas. Ativar esta chave permitirá o Stash a compartilhar as informações da cena atual com handyfeeling.com", "heading": "Chave de conexão" }, + "image_lightbox": { + "heading": "Galeria de imagem" + }, "images": { "heading": "Imagens", "options": { @@ -499,13 +531,17 @@ "scene_wall": { "heading": "Muro de cenas/marcadores", "options": { - "display_title": "Exibir título e tags", + "display_title": "Exibir título e etiquetas", "toggle_sound": "Habilitar som" } }, + "scroll_attempts_before_change": { + "description": "Número de vezes para tentar rolar antes de passar para o próximo/prévio item. Só se aplica ao modo de rolagem Movimentar Y.", + "heading": "Tentativas de rolagem antes da transição" + }, "slideshow_delay": { "description": "Slideshow está disponível em galerias quando no modo de exibição de paredão", - "heading": "Atraso do slideshow" + "heading": "Atraso do slideshow (segundos)" }, "title": "Interface de usuário" } @@ -520,7 +556,7 @@ "performers": "{count, plural, one {Artista} other {Artistas}}", "scenes": "{count, plural, one {Cena} other {Cenas}}", "studios": "{count, plural, one {Estúdio} other {Estúdios}}", - "tags": "{count, plural, one {Tag} other {Tags}}" + "tags": "{count, plural, one {Etiqueta} other {Etiquetas}}" }, "country": "País", "cover_image": "Imagem de capa", @@ -558,7 +594,7 @@ "aliases_must_be_unique": "apelidos devem ser únicos", "delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:", "delete_confirm": "Tem certeza de que deseja excluir {entityName}?", - "delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também seja excluído, este(a) {singularEntity} será re-adicionado quando a varredura for executada.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também sejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando a varredura for executada.}}", + "delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}", "delete_entity_title": "{count, plural, one {Excluir {singularEntity}} other {Excluir {pluralEntity}}}", "delete_galleries_extra": "...e qualquer imagem não anexada a uma galeria.", "delete_gallery_files": "Deletar diretório, arquivo zip ou imagem não anexada a alguma galeria.", @@ -580,12 +616,12 @@ "reset_zoom_on_nav": "Restaurar nível de zoom ao trocar de imagem", "scale_up": { "description": "Aumentar imagens menores até que preencham a tela", - "label": "Aumentar para caber" + "label": "Aumentar até caber" }, "scroll_mode": { "description": "Mantenha shift pressionado para usar outro modo temporariamente.", - "label": "Modo scroll", - "pan_y": "Pan Y", + "label": "Modo de rolagem", + "pan_y": "Movimentar Y", "zoom": "Zoom" } }, @@ -602,10 +638,12 @@ "interactive_heatmap_speed": "Gerar heatmaps e velocidades para cenas interativas", "marker_image_previews": "Pré-visualizações de Imagem Animada para Marcadores", "marker_image_previews_tooltip": "Pré-visualizações WebP animadas para marcadores, necessário apenas de o Tipo de Pré-visualização estiver configurado para Imagem Animada.", - "marker_screenshots": "Capturas de Tela de Marcadores", + "marker_screenshots": "Capturas de tela de Marcadores", "marker_screenshots_tooltip": "Imagens JPG estáticas para marcadores, necessário apenas se o Tipo de Pré-visualização estiver configurado para Imagem Estática.", "markers": "Pré-visualizações de Marcadores", "markers_tooltip": "Vídeos de 20 segundos que iniciam em dado código de tempo.", + "override_preview_generation_options": "Sobrepor as opções da geração de pré-visualização", + "override_preview_generation_options_desc": "Sobrepor as opções da geração de pré-visualização. Os padrões são definidos em Sistema -> Geração de pré-visualização.", "overwrite": "Substituir arquivos gerados existentes", "phash": "Hashes perceptivos (para desduplicação)", "preview_exclude_end_time_desc": "Excluir os últimos x segundos de pré-visualizações de cena. Isso pode ser um valor em segundos, ou uma porcentagem (p. ex. 2%) da duração total da cena.", @@ -638,9 +676,9 @@ "dimensions": "Dimensões", "director": "Diretor(a)", "display_mode": { - "grid": "Grid", + "grid": "Grade", "list": "Lista", - "tagger": "Tagger", + "tagger": "Etiquetador", "unknown": "Desconhecido(a)", "wall": "Paredão" }, @@ -662,7 +700,7 @@ "effect_filters": { "aspect": "Aspecto", "blue": "Azul", - "blur": "Blur", + "blur": "Borrão", "brightness": "Brilho", "contrast": "Contraste", "gamma": "Gama", @@ -680,7 +718,9 @@ "scale": "Escala", "warmth": "Calor" }, + "empty_server": "Adicione algumas cenas ao seu servidor para ver as recomendações nesta página.", "ethnicity": "Etnicidade", + "existing_value": "valor existente", "eye_color": "Cor dos olhos", "fake_tits": "Peitos falsos", "false": "Falso", @@ -695,6 +735,12 @@ "filters": "Filtros", "framerate": "Taxa de quadros", "frames_per_second": "{value} quadros por segundo", + "front_page": { + "types": { + "premade_filter": "Filtro pré-pronto", + "saved_filter": "Filtro salvo" + } + }, "galleries": "Galerias", "gallery": "Galeria", "gallery_count": "Contagem de galeria", @@ -708,15 +754,25 @@ "TRANSGENDER_MALE": "Transgênero Masculino" }, "hair_color": "Cor do cabelo", + "handy_connection_status": { + "connecting": "Conectando", + "disconnected": "Desconectado", + "error": "Erro conectando ao Handy", + "missing": "Faltando", + "ready": "Pronto", + "syncing": "Sincronizando com o servidor", + "uploading": "Enviando script" + }, "hasMarkers": "Possui marcadores", "height": "Altura", "help": "Ajuda", + "ignore_auto_tag": "Ignorar etiquetamento automático", "image": "Imagem", "image_count": "Contagem de imagem", "images": "Imagens", - "include_parent_tags": "Incluir tags pai", + "include_parent_tags": "Incluir etiquetas pai", "include_sub_studios": "Incluem estúdios filho", - "include_sub_tags": "Incluir sub-tags", + "include_sub_tags": "Incluir sub-etiquetas", "instagram": "Instagram", "interactive": "Interativo", "interactive_speed": "Velocidade interativa", @@ -739,7 +795,7 @@ "age_context": "{age} {years_old} nesta cena" }, "phash": "PHash", - "stream": "Stream", + "stream": "Transmissão", "video_codec": "Codec de vídeo" }, "megabits_per_second": "{value} megabits por segundo", @@ -761,13 +817,13 @@ }, "parent_of": "Pai de {children}", "parent_studios": "Estúdios pai", - "parent_tag_count": "Contador de tags pai", - "parent_tags": "Tags pai", + "parent_tag_count": "Contador de etiquetas pai", + "parent_tags": "Etiquetas pai", "part_of": "Parte de {parent}", "path": "Caminho", "perceptual_similarity": "Semelhança Perceptiva (phash)", "performer": "Artista", - "performerTags": "Tags de artitas", + "performerTags": "Etiquetas de artistas", "performer_age": "Idade do Artista", "performer_count": "Contagem de artistas", "performer_favorite": "Artista Favoritado", @@ -788,34 +844,36 @@ "current_page": "Página atual", "failed_to_save_performer": "Falha ao salvar artista \"{performer}\"", "name_already_exists": "Nome já existe", - "network_error": "Erro de Rede", + "network_error": "Erro de rede", "no_results_found": "Nenhum resultado encontrado.", "number_of_performers_will_be_processed": "{performer_count} artistas serão processados", - "performer_already_tagged": "Artista já taggeado", + "performer_already_tagged": "Artista já etiquetado", "performer_names_separated_by_comma": "Nomes de artistas separados por vírgula", "performer_selection": "Seleção de artista", - "performer_successfully_tagged": "Artista taggeado com sucesso:", + "performer_successfully_tagged": "Artista etiquetado com sucesso:", "query_all_performers_in_the_database": "Todos os artistas no banco de dados", - "refresh_tagged_performers": "Recarregar artistas taggeados", - "refreshing_will_update_the_data": "Recarregar irá atualizar os dados de qualquer artista taggeado da instância do stash-box.", - "status_tagging_job_queued": "Status: Taggeamento adicionado à fila", - "status_tagging_performers": "Status: Taggeando artistas", - "tag_status": "Status da Tag", - "to_use_the_performer_tagger": "Para usar o tagger de artistas, uma instância do stash-box deve ser configurada.", - "untagged_performers": "Artistas sem tag", + "refresh_tagged_performers": "Recarregar artistas etiquetados", + "refreshing_will_update_the_data": "Recarregar irá atualizar os dados de qualquer artista etiquetado da instância do stash-box.", + "status_tagging_job_queued": "Status: Etiquetamento adicionado à fila", + "status_tagging_performers": "Status: Etiquetando artistas", + "tag_status": "Status da etiqueta", + "to_use_the_performer_tagger": "Para usar o etiquetador de artistas, uma instância do stash-box deve ser configurada.", + "untagged_performers": "Artistas sem etiqueta", "update_performer": "Atualizar Artista", "update_performers": "Atualizar Artistas", - "updating_untagged_performers_description": "A atualização de artistas sem tag tentará corresponder a qualquer artista sem um shashid e atualizar os metadados." + "updating_untagged_performers_description": "A atualização de artistas sem etiqueta tentará corresponder a qualquer artista sem um stashid e atualizar os metadados." }, "performers": "Artistas", "piercings": "Piercings", "queue": "Fila", "random": "Aleatória", "rating": "Avaliação", + "recently_added_objects": "{objects} Recentemente Adicionados", + "recently_released_objects": "{objects} Recentemente Lançados", "resolution": "Resolução", "scene": "Cena", - "sceneTagger": "Tagger de cena", - "sceneTags": "Tags de cena", + "sceneTagger": "Etiquetador de cena", + "sceneTags": "Etiquetas da cena", "scene_count": "Contagem de cena", "scene_id": "Cena ID", "scenes": "Cenas", @@ -848,6 +906,10 @@ "something_went_wrong_description": "Se isso parece um problema com os dados fornecidos, clique no em Voltar para corrigi-los. Caso contrário, reporte um bug em {githubLink} ou busque ajuda em {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Algo deu errado enquanto configurávamos seu sistema. Aqui está o erro que recebemos: {error}" }, + "folder": { + "file_path": "Caminho do arquivo", + "up_dir": "Subir um diretório" + }, "github_repository": "Repositório do Github", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Caminho para o backup do banco de dados (deixe em branco para desabilitar o backup):", @@ -901,10 +963,7 @@ "next_step": "Quando estiver pronto para prosseguir com a criação do novo sistema, clique Próximo.", "unable_to_locate_specified_config": "Se está lendo isto, então o Stash não pôde encontrar o arquivo de configuração especificado na linha de comando ou no ambiente. Este assistente irá te guiar durante o processo de criação de uma nova configuração." }, - "welcome_to_stash": "Bem-vindo ao Stash", - "folder": { - "up_dir": "Subir um diretório" - } + "welcome_to_stash": "Bem-vindo ao Stash" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", @@ -912,8 +971,10 @@ "go_review_draft": "Vá para {endpoint_name} para revisar rascunho.", "selected_stash_box": "Endpoint do Stash-Box selecionado", "submission_failed": "Falha no envio", - "submission_successful": "Envio bem-sucedido" + "submission_successful": "Envio bem-sucedido", + "submit_update": "Já existe no {endpoint_name}" }, + "statistics": "Estatísticas", "stats": { "image_size": "Tamanho das imagens", "scenes_duration": "Duração das cenas", @@ -923,14 +984,14 @@ "studio": "Estúdio", "studio_depth": "Níveis (vazio para todos)", "studios": "Estúdios", - "sub_tag_count": "Número de sub-tags", - "sub_tag_of": "Sub-tags de {parent}", - "sub_tags": "Sub-tags", + "sub_tag_count": "Contagem de sub-etiquetas", + "sub_tag_of": "Sub-etiqueta de {parent}", + "sub_tags": "Sub-etiquetas", "subsidiary_studios": "Estúdios filhos", "synopsis": "Sinopse", - "tag": "Tag", - "tag_count": "Número de tags", - "tags": "Tags", + "tag": "Etiqueta", + "tag_count": "Contagem de etiquetas", + "tags": "Etiquetas", "tattoos": "Tatuagens", "title": "Título", "toast": { @@ -941,10 +1002,10 @@ "delete_entity": "Excluir {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "delete_past_tense": "Excluída {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Gerando captura de tela…", - "merged_tags": "Tags mescladas", + "merged_tags": "Etiquetas mescladas", "rescanning_entity": "Reescaneando {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "{entity} salvo(a)", - "started_auto_tagging": "Auto tagging iniciado", + "started_auto_tagging": "Etiquetamento automático iniciado", "started_generating": "Geração de arquivos multimídia iniciada", "started_importing": "Importação iniciada", "updated_entity": "{entity} atualizado(a)" @@ -952,9 +1013,11 @@ "total": "Total", "true": "Verdadeiro", "twitter": "Twitter", + "type": "Tipo", "updated_at": "Atualizado em", "url": "URL", "videos": "Vídeos", + "view_all": "Ver todos", "weight": "Peso", "years_old": "anos" } diff --git a/ui/v2.5/src/locales/ro-RO.json b/ui/v2.5/src/locales/ro-RO.json index 2f6caa5b1..cdd229273 100644 --- a/ui/v2.5/src/locales/ro-RO.json +++ b/ui/v2.5/src/locales/ro-RO.json @@ -1,51 +1,466 @@ { "actions": { - "add": "Adăuga", + "add": "Adaugă", "add_directory": "Adaugă directorul", + "add_entity": "Adaugă {entityType}", + "add_to_entity": "Adaugă la {entityType}", "allow": "Permite", "allow_temporarily": "Permite temporar", + "apply": "Aplică", + "auto_tag": "Etichetă automată", + "backup": "Backup", + "browse_for_image": "Răsfoiți pentru imagine…", "cancel": "Anulați", - "clean": "Curăța", - "clear": "Elimina", + "clean": "Curăță", + "clear": "Elimină", + "clear_image": "Curăță Imaginea", "close": "Închide", "confirm": "Confirmare", - "create": "Crea", - "create_entity": "Crea {entityType}", + "continue": "Continuă", + "create": "Creați", + "create_entity": "Creați {entityType}", + "create_marker": "Creați marcator", + "created_entity": "S-a creat {entity_type}: {entity_name}", + "customise": "Personalizați", "delete": "Șterge", "delete_entity": "Șterge {entityType}", "delete_file": "Șterge fişier", "delete_file_and_funscript": "Șterge fişier (şi funscript)", + "delete_stashid": "Șterge StashID", "disallow": "Nu se permite", "download": "Descărcare", + "download_backup": "Descarcă Backup", "edit": "Editare", + "edit_entity": "Editați {entityType}", "export": "Export…", + "export_all": "Exportați tot…", "find": "Găsire", "finish": "Terminare", "from_file": "Din fișier…", "from_url": "Din URL…", + "full_export": "Export Total", + "full_import": "Importă Tot", "generate": "Generare", + "generate_thumb_default": "Genereaza miniatură implicită", + "generate_thumb_from_current": "Generează miniatura de la momentul curent", "hide": "Ascundere", + "hide_configuration": "Ascunde Configurația", "identify": "Identificare", "ignore": "Ignorare", "import": "Import…", + "import_from_file": "Importă din Fișier", + "logout": "Deconectare", "merge": "Îmbinare", "next_action": "Următorul", + "not_running": "nu rulează", + "open_in_external_player": "Deschide in player extern", + "open_random": "Deschide Aleatoriu", "overwrite": "Suprascriere", + "play_random": "Redă Aleatoriu", + "play_selected": "Redă selecția", "preview": "Previzualizare", "previous_action": "Înapoi", "refresh": "Reîmprospătare", + "reload_plugins": "Reîncărcați plugin-urile", + "reload_scrapers": "Reîncărcați scraperele", "remove": "Eliminare", + "remove_from_gallery": "Șterge din Galerie", + "rename_gen_files": "Redenumiți fișierele generate", + "rescan": "Rescanare", + "running": "rulează", "save": "Salvare", + "save_delete_settings": "Folosiți aceste opțiuni în mod implicit atunci cănd ștergeți", + "save_filter": "Salvați filtru", "scan": "Scanare", "scrape": "Extrage", "search": "Căutare", "select_all": "Selectare totală", + "select_entity": "Selectează {entityType}", "select_folders": "Selectare foldere", "select_none": "Selectare niciuna", "set_as_default": "Stabilire ca implicit", + "set_image": "Setează imaginea…", "show": "Afișare", + "show_configuration": "Arată Configurația", "skip": "Ignorare", "stop": "Oprire", - "submit": "Trimite" - } + "submit": "Trimite", + "submit_stash_box": "Trimite către Stash-Box", + "submit_update": "Trimite actualizare", + "tasks": { + "clean_confirm_message": "Ești sigur că vrei să cureți? Acest lucru va șterge informațiile din baza de date și conținutul generat pentru toate scenele și galeriile care nu se mai găsesc în sistemul de fișiere.", + "import_warning": "Ești sigur că vrei să imporți? Asta va șterge baza de date si va reimporta din metadatele tale exporatate." + }, + "temp_disable": "Dezactivează temporar…", + "temp_enable": "Activează temporar…", + "use_default": "Folosește implicit", + "view_random": "Vezi Aleatoriu" + }, + "actions_name": "Acțiuni", + "age": "Vârstă", + "all": "toate", + "also_known_as": "Cunoscut/ă și ca", + "ascending": "Ascendent", + "average_resolution": "Rezoluție medie", + "birth_year": "Anul Nașterii", + "birthdate": "Data nașterii", + "bitrate": "Bit Rate", + "career_length": "Durata carierei", + "component_tagger": { + "config": { + "active_instance": "Instanță stash-box activă:", + "blacklist_label": "Lista neagră", + "query_mode_auto": "Automat", + "query_mode_auto_desc": "Folosește metadatele, dacă sunt prezente, sau numele fișierului", + "query_mode_dir": "Dir", + "query_mode_dir_desc": "Folosește numai directorul părinte al fișierului video", + "query_mode_filename": "Numele fișierului", + "query_mode_filename_desc": "Folosește numai numele fișierului", + "query_mode_metadata_desc": "Folosește numai metadate", + "query_mode_path": "Cale", + "query_mode_path_desc": "Utilizează întreaga cale a fișierului", + "set_cover_desc": "Înlocuiți coperta scenei, dacă se găsește una.", + "set_cover_label": "Setează imaginea de copertă a scenei", + "set_tag_desc": "Atașați etichete scenei, fie prin suprascriere, fie prin fuziune cu etichetele existente pe scenă.", + "set_tag_label": "Setați etichete", + "show_male_desc": "Comutați dacă interpreții de sex masculin vor fi disponibili pentru etichetare.", + "show_male_label": "Arată interpreți de sex masculin", + "source": "Sursă" + }, + "results": { + "duration_unknown": "Durată necunoscută", + "match_failed_already_tagged": "Scena este deja etichetată", + "match_failed_no_result": "Nu s-au găsit rezultate", + "match_success": "Scena a fost etichetată cu succes", + "unnamed": "Fără denumire" + }, + "verb_toggle_config": "{toggle} {configuration}" + }, + "config": { + "about": { + "check_for_new_version": "Verifică pentru o versiune nouă", + "latest_version": "Ultima Versiune", + "new_version_notice": "[NOU]", + "stash_discord": "Alăturați-vă {url} canalului nostru", + "stash_open_collective": "Susține-ne prin {url}", + "version": "Versiune" + }, + "application_paths": { + "heading": "Cale Aplicație" + }, + "categories": { + "about": "Despre", + "interface": "Interfață", + "metadata_providers": "Furnizori Metadate", + "plugins": "Plugin-uri", + "security": "Securitate", + "services": "Servicii", + "system": "Sistem", + "tasks": "Sarcini", + "tools": "Unelte" + }, + "dlna": { + "allow_temp_ip": "Permite {tempIP}", + "allowed_ip_addresses": "Adrese IP permise", + "allowed_ip_temporarily": "IP permis temporar", + "default_ip_whitelist_desc": "Adresele IP implicite permit accesul la DLNA. Utilizați {wildcard} pentru a permite toate adresele IP.", + "disabled_dlna_temporarily": "Dezactivat temporar DLNA", + "disallowed_ip": "IP nepermis", + "enabled_by_default": "Activat în mod implicit", + "enabled_dlna_temporarily": "Activat DLNA temporar", + "network_interfaces": "Intefețe", + "network_interfaces_desc": "Interfețe pentru a expune serverul DLNA. O listă goală are ca rezultat rularea pe toate interfețele. Necesită repornirea DLNA după modificare.", + "recent_ip_addresses": "Adrese IP recente", + "successfully_cancelled_temporary_behaviour": "S-a anulat cu succes comportamentul temporar", + "until_restart": "până la repornire" + }, + "general": { + "auth": { + "api_key": "Cheie API", + "api_key_desc": "Cheia API pentru sistemele externe. Necesară numai atunci când este configurat numele de utilizator/parola. Numele de utilizator trebuie salvat înainte de a genera cheia API.", + "authentication": "Autentificare", + "clear_api_key": "Ștergeți cheia API", + "credentials": { + "description": "Credențiale pentru a restricționa accesul la Stash.", + "heading": "Credențiale" + }, + "generate_api_key": "Generează cheie API" + }, + "cache_path_head": "Cale Cache", + "calculate_md5_and_ohash_label": "Calculați MD5 pentru videouri" + }, + "tasks": { + "auto_tagging": "Etichetare Automată", + "cleanup_desc": "Verifică dacă există fișiere lipsă și le elimină din baza de date. Aceasta este o acțiune distructivă." + }, + "tools": { + "scene_filename_parser": { + "capitalize_title": "Capitalizați titlul" + } + }, + "ui": { + "basic_settings": "Setări de bază", + "handy_connection": { + "connect": "Conectare" + }, + "preview_type": { + "options": { + "animated": "Imagine Animată" + } + } + } + }, + "configuration": "Configurație", + "country": "Țară", + "cover_image": "Imagine de Copertă", + "created_at": "Creat La", + "dialogs": { + "delete_confirm": "Ești sigur ca vrei să ștergi {entityName}?", + "overwrite_filter_confirm": "Sunteți sigur că doriți să suprascrieți interogarea salvată existentă {entityName}?", + "scene_gen": { + "force_transcodes_tooltip": "În mod implicit, transcodurile sunt generate numai atunci când fișierul video nu este acceptat în browser. Atunci când este activată, transcodurile vor fi generate chiar și atunci când fișierul video pare a fi acceptat în browser.", + "image_previews": "Imagini animate de previzualizare", + "image_previews_tooltip": "Previzualizări WebP animate, necesare numai dacă tipul de previzualizare este setat pe Imagine animată." + } + }, + "director": "Director", + "display_mode": { + "grid": "Grilă", + "list": "Listă", + "unknown": "Necunoscut", + "wall": "Perete" + }, + "donate": "Donează", + "dupe_check": { + "options": { + "exact": "Precis", + "high": "Sus", + "low": "Jos", + "medium": "Mediu" + }, + "search_accuracy_label": "Precizia căutării", + "title": "Scene Duplicate" + }, + "duration": "Durată", + "effect_filters": { + "aspect": "Aspect", + "blue": "Albastru", + "blur": "Blur", + "brightness": "Luminozitate", + "contrast": "Contrast", + "gamma": "Gamma", + "green": "Verde", + "hue": "Nuanța culorii", + "name": "Filtre", + "name_transforms": "Transformări", + "red": "Roșu", + "reset_filters": "Resetează Filtre", + "reset_transforms": "Resetează Transformări", + "rotate": "Rotire", + "saturation": "Saturație", + "scale": "Scală", + "warmth": "Căldură" + }, + "empty_server": "Adăugați câteva scene pe serverul dvs. pentru a vedea recomandările de pe această pagină.", + "ethnicity": "Etnie", + "existing_value": "valoare existentă", + "eye_color": "Culoarea Ochilor", + "fake_tits": "Sâni falși", + "false": "Fals", + "favourite": "Favorit", + "file": "fișier", + "file_info": "Informații Fișier", + "files": "fișiere", + "filesize": "Dimensiune Fișier", + "filter": "Filtru", + "filter_name": "Nume Filtru", + "filters": "Filtre", + "framerate": "Rata de cadre", + "frames_per_second": "{value} cadre pe secundă", + "front_page": { + "types": { + "saved_filter": "Filtru Salvat" + } + }, + "galleries": "Galerii", + "gallery": "Galerie", + "gallery_count": "Numărul de galerii", + "gender": "Gen", + "gender_types": { + "FEMALE": "Feminin", + "INTERSEX": "Intersex", + "MALE": "Masculin", + "NON_BINARY": "Non-Binar", + "TRANSGENDER_FEMALE": "Femeie Transgender", + "TRANSGENDER_MALE": "Bărbat Transgender" + }, + "hair_color": "Culoarea Părului", + "handy_connection_status": { + "connecting": "Se conectează", + "disconnected": "Deconectat", + "missing": "Lipsă", + "ready": "Pregătit", + "syncing": "Se sincronizează cu serverul" + }, + "height": "Inălțime", + "help": "Ajutor", + "ignore_auto_tag": "Ignoră Etichetele Automate", + "image": "Imagine", + "image_count": "Număr Imagine", + "images": "Imagini", + "instagram": "Instagram", + "interactive": "Interactiv", + "interactive_speed": "Viteză interactivă", + "isMissing": "Lipsește", + "library": "Librărie", + "loading": { + "generic": "Se încarcă…" + }, + "markers": "Marcatori", + "measurements": "Măsurători", + "media_info": { + "audio_codec": "Codec Audio", + "checksum": "Sumă de control", + "hash": "Hash", + "interactive_speed": "Viteză Interactivă", + "performer_card": { + "age": "{age} {years_old}", + "age_context": "{age} {years_old} în această scenă" + }, + "phash": "PHash", + "stream": "Flux", + "video_codec": "Codec Video" + }, + "megabits_per_second": "{value} megabiți pe secundă", + "metadata": "Metadate", + "movie": "Film", + "movies": "Filme", + "name": "Nume", + "new": "Nou", + "o_counter": "O-Contor", + "operations": "Operațiuni", + "organized": "Organizat", + "pagination": { + "first": "Primul", + "last": "Ultimul", + "next": "Următorul", + "previous": "Anterior" + }, + "parent_of": "Părintele {children}", + "path": "Cale", + "performer": "Interpret", + "performerTags": "Eticheta Interpret", + "performer_age": "Vârstă Interpret", + "performer_image": "Imagine Interpret", + "performer_tagger": { + "add_new_performers": "Adaugă Interpreți Noi", + "config": { + "active_stash-box_instance": "Instață stash-box activă:", + "edit_excluded_fields": "Editați câmpurile excluse", + "no_fields_are_excluded": "Nu se exclude niciun câmp", + "no_instances_found": "Nicio instanță găsită", + "these_fields_will_not_be_changed_when_updating_performers": "Aceste câmpuri nu vor fi modificate la actualizarea artiștilor interpreți sau executanți." + }, + "current_page": "Pagina curentă", + "failed_to_save_performer": "Nu s-a reușit salvarea interpretului \"{performer}\"", + "name_already_exists": "Numele există deja", + "network_error": "Eroare Rețea", + "no_results_found": "Nu s-a găsit niciun rezultat.", + "performer_already_tagged": "Interpret deja etichetat", + "performer_successfully_tagged": "Interpretul a fost etichetat cu succes:", + "query_all_performers_in_the_database": "Toti interpreții din baza de date", + "refresh_tagged_performers": "Reîmprospătați interpeții etichetați", + "tag_status": "Stare Etichetă", + "untagged_performers": "Interpreți neetichetați", + "update_performer": "Actualizați Interpret", + "update_performers": "Actualizați Interpreți" + }, + "performers": "Interpreți", + "piercings": "Piercing-uri", + "queue": "Coadă", + "random": "Aleatoriu", + "rating": "Evaluare", + "recently_added_objects": "Recent Adăugat {objects}", + "recently_released_objects": "Recent lansate {obiecte}", + "resolution": "Rezoluție", + "scene": "Scenă", + "scene_id": "ID Scenă", + "scenes": "Scene", + "scenes_updated_at": "Scenă Actualizată La", + "search_filter": { + "add_filter": "Adaugă Filtru", + "name": "Filtru", + "saved_filters": "Filtre Salvate", + "update_filter": "Actualizează Filtru" + }, + "seconds": "Secunde", + "settings": "Setări", + "setup": { + "confirm": { + "almost_ready": "Suntem aproape gata să finalizăm configurația. Vă rugăm să confirmați următoarele setări. Puteți face clic înapoi pentru a modifica orice lucru incorect. Dacă totul pare în regulă, faceți clic pe Confirm (Confirmare) pentru a vă crea sistemul.", + "configuration_file_location": "Locația fișierului de configurare:", + "database_file_path": "Cale fișier bază de date", + "default_generated_content_location": "/generated", + "generated_directory": "Director generat", + "nearly_there": "Aproape de final!" + }, + "creating": { + "creating_your_system": "Crearea sistemului dvs", + "ffmpeg_notice": "Dacă ffmpeg nu se află încă în căile dvs. de acces, vă rugăm să aveți răbdare în timp ce stash îl descarcă. Vizualizați consola pentru a vedea progresul descărcării." + }, + "errors": { + "something_went_wrong": "O nu! Ceva nu a mers bine!", + "something_went_wrong_description": "Dacă aceasta pare a fi o problemă cu intrările dvs., dați click înapoi pentru a le repara. În caz contrar, creați un bug pe {githubLink} sau căutați ajutor în {discordLink}.", + "something_went_wrong_while_setting_up_your_system": "Ceva nu a mers bine in timp ce configuram sistemul tău. Aceasta e eroarea: {error}" + }, + "github_repository": "Repozitoriu Github", + "success": { + "open_collective": "Consultați {open_collective_link} pentru a vedea cum puteți contribui la dezvoltarea continuă a Stash." + } + }, + "stash_id": "ID Stash", + "stash_ids": "ID-uri Stash", + "stashbox": { + "go_review_draft": "Mergi la {endpoint_name} pentru a revizui schița.", + "submission_failed": "Trimiterea a eșuat", + "submission_successful": "Depunere reușită", + "submit_update": "Deja există în {endpoint_name}" + }, + "statistics": "Statistici", + "stats": { + "scenes_duration": "Durată scene", + "scenes_size": "Dimensiuni scene" + }, + "status": "Stare: {statusText}", + "studio": "Studio", + "studio_depth": "Niveluri (lasați liber pentru toate)", + "studios": "Studiouri", + "synopsis": "Sinopsis", + "tag": "Etichetă", + "tag_count": "Număr Etichete", + "tags": "Etichete", + "tattoos": "Tatuaje", + "title": "Titlu", + "toast": { + "added_entity": "Adăugat {entity}", + "added_generation_job_to_queue": "S-a adăugat sarcina de generare la coadă", + "created_entity": "S-a creat {entity}", + "generating_screenshot": "Se genereaza captura de ecran…", + "merged_tags": "Etichete fuzionate", + "saved_entity": "Salvat {entity}", + "started_auto_tagging": "A început etichetarea automată", + "started_generating": "A început generarea", + "started_importing": "A început importarea", + "updated_entity": "Actualizat {entity}" + }, + "total": "Total", + "true": "Adevărat", + "twitter": "Twitter", + "type": "Tip", + "updated_at": "Actualizat La", + "url": "URL", + "videos": "Videouri", + "view_all": "Vezi Toate", + "weight": "Greutate", + "years_old": "ani" } diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index c26b67589..05f6fce9f 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -23,6 +23,7 @@ "create_entity": "Skapa {entityType}", "create_marker": "Skapa markör", "created_entity": "Skapade {entity_type}: {entity_name}", + "customise": "Ändra", "delete": "Radera", "delete_entity": "Radera {entityType}", "delete_file": "Radera fil", @@ -100,6 +101,7 @@ "stop": "Stoppa", "submit": "Skicka", "submit_stash_box": "Skicka till Stash-Box", + "submit_update": "Skicka uppdatering", "tasks": { "clean_confirm_message": "Är du säker att du vill rensa? Detta kommer radera databasinformation och genererade filer för alla scener och gallerier som inte längre finns på filsystemet.", "dry_mode_selected": "Torrt läge valt. Inget kommer raderas utan bara loggning kommer ske.", @@ -733,6 +735,12 @@ "filters": "Filter", "framerate": "Bildhastighet", "frames_per_second": "{value} bilder per sekund", + "front_page": { + "types": { + "premade_filter": "Förhandsgjorda filter", + "saved_filter": "Sparade Filter" + } + }, "galleries": "Gallerier", "gallery": "Galleri", "gallery_count": "Antal Gallerier", @@ -860,11 +868,8 @@ "queue": "Kö", "random": "Slumpad", "rating": "Betyg", - "recently_added_performers": "Nyligen Tillagda Stjärnor", - "recently_added_studios": "Nyligen Tillagda Studior", - "recently_released_galleries": "Nyligen Släppta Gallerier", - "recently_released_movies": "Nyligen Släppta Filmer", - "recently_released_scenes": "Nyligen Släppta Scener", + "recently_added_objects": "Nyligen Tillagda {objects}", + "recently_released_objects": "Nyligen Släppta {objects}", "resolution": "Upplösning", "scene": "Scen", "sceneTagger": "Scentaggaren", @@ -966,7 +971,8 @@ "go_review_draft": "Gå till {endpoint_name} för att granska utkast.", "selected_stash_box": "Vald stash-box adress", "submission_failed": "Misslyckad inskickning", - "submission_successful": "Lyckad inskickning" + "submission_successful": "Lyckad inskickning", + "submit_update": "Existerar redan i {endpoint_name}" }, "statistics": "Statistik", "stats": { @@ -1007,6 +1013,7 @@ "total": "Total", "true": "Sant", "twitter": "Twitter", + "type": "Typ", "updated_at": "Uppdaterad vid", "url": "URL", "videos": "Videor", diff --git a/ui/v2.5/src/locales/th-TH.json b/ui/v2.5/src/locales/th-TH.json new file mode 100644 index 000000000..ff59f2a02 --- /dev/null +++ b/ui/v2.5/src/locales/th-TH.json @@ -0,0 +1,47 @@ +{ + "actions": { + "add": "เพิ่ม", + "add_directory": "เพิ่มโฟลเดอร์", + "add_entity": "เพิ่ม{entityType}", + "add_to_entity": "เพิ่มไปยัง{entityType}", + "allow": "อนุญาต", + "allow_temporarily": "อนุญาตชั่วคราว", + "apply": "ใช้งาน", + "auto_tag": "แท็กอัตโนมัติ", + "backup": "สำรองข้อมูล", + "browse_for_image": "เลือกรูป…", + "cancel": "ยกเลิก", + "clean": "เก็บกวาด", + "clear": "ล้างค่า", + "clear_image": "ล้างค่ารูป", + "close": "ปิด", + "confirm": "ยืนยัน", + "continue": "ถัดไป", + "create": "สร้าง", + "create_entity": "สร้าง{entityType}", + "create_marker": "สร้างจุดมาร์ค", + "created_entity": "สร้าง{entity_type}: {entity_name}", + "customise": "ปรับแต่ง", + "delete": "ลบ", + "delete_entity": "ลบ{entityType}", + "delete_file": "ลบไฟล์", + "delete_file_and_funscript": "ลบไฟล์ (และ funscript)", + "delete_generated_supporting_files": "ลบไฟล์ที่เกี่ยวข้อง", + "delete_stashid": "ลบ StashID", + "disallow": "ไม่อนุญาต", + "download": "ดาวน์โหลด", + "download_backup": "ดาวน์โหลดข้อมูลสำรอง", + "edit": "แก้ไข", + "edit_entity": "แก้ไข{entityType}", + "export": "ส่งออก…", + "export_all": "ส่งออกทั้งหมด…", + "find": "ค้นหา", + "finish": "เสร็จ", + "from_file": "จากไฟล์…", + "from_url": "จาก URL…", + "full_export": "ส่งออกทั้งหมด", + "full_import": "นำเข้าทั้งหมด", + "generate": "ผลิต", + "generate_thumb_default": "ผลิตรูปขนาดย่อตั้งต้น" + } +} diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 82c7ca47a..90bfe9c78 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -23,6 +23,7 @@ "create_entity": "创建 {entityType}", "create_marker": "创建标记", "created_entity": "已经创建 {entity_type}: {entity_name}", + "customise": "自定义", "delete": "删除", "delete_entity": "删除 {entityType}", "delete_file": "删除文件", @@ -100,6 +101,7 @@ "stop": "停止", "submit": "提交", "submit_stash_box": "提交给 Stash-Box", + "submit_update": "提交更新", "tasks": { "clean_confirm_message": "确定要清除吗? 这将删除系统中不存在的所有短片和图库的数据库信息和已经生成的内容。", "dry_mode_selected": "已经选择了模拟删除模式。不会实际删除文件,只会写下记录。", @@ -121,6 +123,7 @@ "birth_year": "出生年份", "birthdate": "出生日期", "bitrate": "比特率", + "captions": "字幕", "career_length": "工龄", "component_tagger": { "config": { @@ -191,7 +194,7 @@ "metadata_providers": "元数据提供者", "plugins": "插件", "scraping": "挖掘", - "security": "安保", + "security": "安全性", "services": "服务", "system": "系统", "tasks": "任务", @@ -449,6 +452,16 @@ "description": "交互式脚本播放的时间偏移量(以毫秒为单位)。", "heading": "Funscript偏移量(毫秒)" }, + "handy_connection": { + "connect": "连接", + "server_offset": { + "heading": "服务器偏差值" + }, + "status": { + "heading": "Handy 连接状态" + }, + "sync": "同步" + }, "handy_connection_key": { "description": "用于互动场景的快速连接密钥。设定此密匙会允许Stash分享你当前的短片资料到handyfeeling.com", "heading": "快速连接密钥" @@ -522,6 +535,10 @@ "toggle_sound": "播放声音" } }, + "scroll_attempts_before_change": { + "description": "在转到下/上一项前需要尝试滑动的次数。仅适用于垂直滚动的模式。", + "heading": "转变所需的滑动尝试次数" + }, "slideshow_delay": { "description": "在影音墙模式下,图库可用幻灯片功能", "heading": "幻灯片延迟(秒)" @@ -701,6 +718,7 @@ "scale": "大小", "warmth": "色温" }, + "empty_server": "增加一些短片到服务器以看到本页面的推荐。", "ethnicity": "人种", "existing_value": "现值", "eye_color": "瞳孔颜色", @@ -717,6 +735,12 @@ "filters": "过滤器", "framerate": "帧率", "frames_per_second": "{value} 帧每秒", + "front_page": { + "types": { + "premade_filter": "预制的过滤器", + "saved_filter": "已保存的过滤器" + } + }, "galleries": "图库", "gallery": "图库", "gallery_count": "图库数量", @@ -730,6 +754,15 @@ "TRANSGENDER_MALE": "跨性别男性" }, "hair_color": "头发颜色", + "handy_connection_status": { + "connecting": "连接中", + "disconnected": "连接已断开", + "error": "连接到 Handy 时出错", + "missing": "无", + "ready": "准备好", + "syncing": "正在和服务器同步", + "uploading": "上传脚本中" + }, "hasMarkers": "含有章节标记", "height": "身高", "help": "说明", @@ -835,6 +868,8 @@ "queue": "序列", "random": "随机", "rating": "评分", + "recently_added_objects": "最近新增的 {objects}", + "recently_released_objects": "最近发行的 {objects}", "resolution": "分辨率", "scene": "短片", "sceneTagger": "短片标记器", @@ -936,8 +971,10 @@ "go_review_draft": "去 {endpoint_name} 检阅草稿。", "selected_stash_box": "选择的 Stash-Box 终端", "submission_failed": "提交失败", - "submission_successful": "成功提交" + "submission_successful": "成功提交", + "submit_update": "已存在于 {endpoint_name}" }, + "statistics": "统计", "stats": { "image_size": "图片大小", "scenes_duration": "短片长度", @@ -976,9 +1013,11 @@ "total": "总共", "true": "真", "twitter": "推特", + "type": "类别", "updated_at": "更新时间", "url": "链接", "videos": "视频", + "view_all": "查看全部", "weight": "体重", "years_old": "岁" } diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index c2d937683..fe4bf7675 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -23,6 +23,7 @@ "create_entity": "建立{entityType}", "create_marker": "建立章節標記", "created_entity": "已建立{entity_type}:{entity_name}", + "customise": "自訂", "delete": "刪除", "delete_entity": "刪除{entityType}", "delete_file": "刪除檔案", @@ -100,6 +101,7 @@ "stop": "停止", "submit": "提交", "submit_stash_box": "提交至 Stash-Box", + "submit_update": "提交更新", "tasks": { "clean_confirm_message": "您確定要進行清理嗎?這將從資料庫及產生的文件中清除已不在的短片及圖庫。", "dry_mode_selected": "已選擇了模擬作業模式。不會進行任何實際刪除作業,只會進行模擬記錄。", @@ -121,6 +123,7 @@ "birth_year": "出生年分", "birthdate": "出生日期", "bitrate": "位元率", + "captions": "字幕", "career_length": "活躍年代", "component_tagger": { "config": { @@ -449,6 +452,16 @@ "description": "互動式腳本的時間偏移量 (毫秒)。", "heading": "Funscript 偏移量 (毫秒)" }, + "handy_connection": { + "connect": "連接", + "server_offset": { + "heading": "伺服器誤差值" + }, + "status": { + "heading": "Handy 連線狀態" + }, + "sync": "同步" + }, "handy_connection_key": { "description": "播放支援互動性的短片時所用的 Handy 連線金鑰。設定此金鑰後,Stash 將可把當前短片中的對應資訊分享至 handyfeeling.com", "heading": "Handy 連線金鑰" @@ -522,6 +535,10 @@ "toggle_sound": "播放聲音" } }, + "scroll_attempts_before_change": { + "description": "在移動到下一項/上一項之前嘗試滑動的次數。僅適用於『Y軸滑動』模式。", + "heading": "場景變換滑動嘗試次數" + }, "slideshow_delay": { "description": "幻燈片功能僅適用於「圖庫」種類下的預覽牆模式", "heading": "幻燈片延遲 (秒)" @@ -701,6 +718,7 @@ "scale": "大小", "warmth": "暖度" }, + "empty_server": "若要啟用影片推薦,請先於伺服器中新增一些短片。", "ethnicity": "人種", "existing_value": "現有值", "eye_color": "眼睛顏色", @@ -730,6 +748,15 @@ "TRANSGENDER_MALE": "跨型別男性" }, "hair_color": "頭髮顏色", + "handy_connection_status": { + "connecting": "連接中", + "disconnected": "已斷線", + "error": "連接至 Handy 時出錯", + "missing": "遺失", + "ready": "已準備", + "syncing": "與伺服器同步中", + "uploading": "上傳腳本中" + }, "hasMarkers": "含有章節標記", "height": "身高", "help": "說明", @@ -835,6 +862,8 @@ "queue": "佇列", "random": "隨機", "rating": "評比", + "recently_added_objects": "最近新增的{objects}", + "recently_released_objects": "最近釋出的{objects}", "resolution": "解析度", "scene": "短片", "sceneTagger": "短片標籤器", @@ -936,8 +965,10 @@ "go_review_draft": "到 {endpoint_name} 預覽草稿。", "selected_stash_box": "已選擇的 Stash-Box 端點", "submission_failed": "提交失敗", - "submission_successful": "提交成功" + "submission_successful": "提交成功", + "submit_update": "已存在於 {endpoint_name}" }, + "statistics": "統計資訊", "stats": { "image_size": "圖片大小", "scenes_duration": "短片長度", @@ -976,9 +1007,11 @@ "total": "總計", "true": "是", "twitter": "Twitter", + "type": "種類", "updated_at": "更新於", "url": "連結", "videos": "影片", + "view_all": "檢視所有", "weight": "體重", "years_old": "歲" } diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index 8f3f7001c..28b330569 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -51,10 +51,13 @@ export function makeCriteria(type: CriterionType = "none") { return new NoneCriterion(); case "name": case "path": - case "checksum": return new StringCriterion( new MandatoryStringCriterionOption(type, type) ); + case "checksum": + return new StringCriterion( + new MandatoryStringCriterionOption("media_info.checksum", type, type) + ); case "oshash": return new StringCriterion( new MandatoryStringCriterionOption("media_info.hash", type, type) @@ -134,7 +137,7 @@ export function makeCriteria(type: CriterionType = "none") { case "sceneChecksum": case "galleryChecksum": return new StringCriterion( - new StringCriterionOption("checksum", type, "checksum") + new StringCriterionOption("media_info.checksum", type, "checksum") ); case "phash": return new StringCriterion(PhashCriterionOption); diff --git a/ui/v2.5/src/models/sceneQueue.ts b/ui/v2.5/src/models/sceneQueue.ts index 948cebc72..3c558493a 100644 --- a/ui/v2.5/src/models/sceneQueue.ts +++ b/ui/v2.5/src/models/sceneQueue.ts @@ -129,3 +129,5 @@ export class SceneQueue { return `/scenes/${sceneID}${params.length ? "?" + params.join("&") : ""}`; } } + +export default SceneQueue; diff --git a/ui/v2.5/src/utils/bulkUpdate.ts b/ui/v2.5/src/utils/bulkUpdate.ts index c91175140..542a57337 100644 --- a/ui/v2.5/src/utils/bulkUpdate.ts +++ b/ui/v2.5/src/utils/bulkUpdate.ts @@ -1,5 +1,5 @@ import * as GQL from "src/core/generated-graphql"; -import _ from "lodash"; +import isEqual from "lodash-es/isEqual"; interface IHasRating { rating?: GQL.Maybe | undefined; @@ -63,7 +63,7 @@ export function getAggregatePerformerIds(state: IHasPerformers[]) { } else { const perfIds = o.performers ? o.performers.map((p) => p.id).sort() : []; - if (!_.isEqual(ret, perfIds)) { + if (isEqual(ret, perfIds)) { ret = []; } } @@ -87,7 +87,7 @@ export function getAggregateTagIds(state: IHasTags[]) { } else { const tIds = o.tags ? o.tags.map((t) => t.id).sort() : []; - if (!_.isEqual(ret, tIds)) { + if (isEqual(ret, tIds)) { ret = []; } } @@ -115,7 +115,7 @@ export function getAggregateMovieIds(state: IHasMovies[]) { } else { const mIds = o.movies ? o.movies.map((m) => m.movie.id).sort() : []; - if (!_.isEqual(ret, mIds)) { + if (isEqual(ret, mIds)) { ret = []; } } @@ -180,7 +180,7 @@ export function getAggregateState( newValue: T, first: boolean ) { - if (!first && !_.isEqual(currentValue, newValue)) { + if (!first && isEqual(currentValue, newValue)) { return undefined; } diff --git a/ui/v2.5/src/utils/country.ts b/ui/v2.5/src/utils/country.ts index d9961a388..68a087c10 100644 --- a/ui/v2.5/src/utils/country.ts +++ b/ui/v2.5/src/utils/country.ts @@ -15,6 +15,7 @@ const fuzzyDict: Record = { "Slovak Republic": "SK", Iran: "IR", Moldova: "MD", + Laos: "LA", }; const getISOCountry = (country: string | null | undefined) => { diff --git a/ui/v2.5/src/utils/data.ts b/ui/v2.5/src/utils/data.ts index 7fc08d3f4..df50a2d93 100644 --- a/ui/v2.5/src/utils/data.ts +++ b/ui/v2.5/src/utils/data.ts @@ -1,7 +1,7 @@ export const filterData = (data?: (T | null | undefined)[] | null) => data ? (data.filter((item) => item) as T[]) : []; -interface ITypename { +export interface ITypename { __typename?: string; } diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index a0a6a33fa..4a9dad558 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -1384,7 +1384,19 @@ dependencies: "@types/node" "*" -"@types/lodash@^4.14.165", "@types/lodash@^4.14.168": +"@types/lodash-es@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0" + integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + +"@types/lodash@^4.14.165": version "4.14.168" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== @@ -2397,6 +2409,11 @@ chardet@^0.7.0: optionalDependencies: fsevents "~2.3.1" +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + classnames@^2.2.6: version "2.2.6" resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz" @@ -4904,7 +4921,7 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20: +lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==